feat(proxy): enhance upstream endpoint handling and error summarization
- Added new utility functions for building minimal JSON headers and normalizing responses for compatibility. - Implemented detailed error handling for unsupported media types and endpoint downgrade scenarios. - Introduced a new endpoint flow mechanism to manage retries and downgrades between different upstream endpoints. - Enhanced tests for endpoint flow and error summarization to cover various edge cases and scenarios. - Updated CSS styles for accounts table to improve layout and responsiveness. - Refactored ModelTester component to handle incremental text updates more effectively.
This commit is contained in:
@@ -503,12 +503,21 @@ export async function accountsRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
// Try to explain unknown failures: missing user id vs anti-bot challenge page.
|
||||
const normalizedPlatform = String(adapter.platformName || site.platform || '').trim().toLowerCase();
|
||||
// New-API family already runs shield-aware probing inside adapters.
|
||||
// Raw fallback probe below does not include challenge-solving and can
|
||||
// misclassify valid Cookie/Session flows as shield-blocked.
|
||||
const skipRawShieldDetection = normalizedPlatform === 'new-api' || normalizedPlatform === 'anyrouter';
|
||||
type VerifyFailureReason = 'needs-user-id' | 'shield-blocked' | null;
|
||||
const detectVerifyFailureReason = async (): Promise<VerifyFailureReason> => {
|
||||
const parseFailureReason = (bodyText: string, contentType: string): VerifyFailureReason => {
|
||||
const text = bodyText || '';
|
||||
const ct = (contentType || '').toLowerCase();
|
||||
if (ct.includes('text/html') && /var\s+arg1\s*=|acw_sc__v2|cdn_sec_tc|<script/i.test(text)) {
|
||||
if (
|
||||
!skipRawShieldDetection
|
||||
&& ct.includes('text/html')
|
||||
&& /var\s+arg1\s*=|acw_sc__v2|cdn_sec_tc|<script/i.test(text)
|
||||
) {
|
||||
return 'shield-blocked';
|
||||
}
|
||||
|
||||
@@ -516,7 +525,9 @@ export async function accountsRoutes(app: FastifyInstance) {
|
||||
const body = JSON.parse(text) as any;
|
||||
const message = typeof body?.message === 'string' ? body.message : '';
|
||||
if (/mismatch|new-api-user|user id/i.test(message)) return 'needs-user-id';
|
||||
if (/shield|challenge|captcha|acw_sc__v2|arg1/i.test(message)) return 'shield-blocked';
|
||||
if (!skipRawShieldDetection && /shield|challenge|captcha|acw_sc__v2|arg1/i.test(message)) {
|
||||
return 'shield-blocked';
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return null;
|
||||
|
||||
@@ -6,9 +6,11 @@ import { join } from 'node:path';
|
||||
|
||||
const verifyTokenMock = vi.fn();
|
||||
const undiciFetchMock = vi.fn();
|
||||
let adapterPlatformName = 'new-api';
|
||||
|
||||
vi.mock('../../services/platforms/index.js', () => ({
|
||||
getAdapter: () => ({
|
||||
platformName: adapterPlatformName,
|
||||
verifyToken: (...args: unknown[]) => verifyTokenMock(...args),
|
||||
}),
|
||||
}));
|
||||
@@ -42,6 +44,7 @@ describe('accounts verify-token shield detection', () => {
|
||||
beforeEach(() => {
|
||||
verifyTokenMock.mockReset();
|
||||
undiciFetchMock.mockReset();
|
||||
adapterPlatformName = 'new-api';
|
||||
|
||||
db.delete(schema.proxyLogs).run();
|
||||
db.delete(schema.checkinLogs).run();
|
||||
@@ -84,7 +87,7 @@ describe('accounts verify-token shield detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns shieldBlocked when upstream /api/user/self responds with challenge html', async () => {
|
||||
it('avoids raw shieldBlocked misclassification for new-api when verifyToken returned tokenType unknown', async () => {
|
||||
verifyTokenMock.mockResolvedValueOnce({ tokenType: 'unknown' });
|
||||
undiciFetchMock.mockResolvedValue({
|
||||
text: async () => '<html><script>var arg1="ABC123";</script></html>',
|
||||
@@ -108,6 +111,75 @@ describe('accounts verify-token shield detection', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: false,
|
||||
message: 'Token invalid: cannot use it as session cookie or API key',
|
||||
});
|
||||
expect(undiciFetchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses adapter platformName to skip raw shield detection for newapi alias site platform', async () => {
|
||||
adapterPlatformName = 'new-api';
|
||||
verifyTokenMock.mockResolvedValueOnce({ tokenType: 'unknown' });
|
||||
undiciFetchMock.mockResolvedValue({
|
||||
text: async () => '<html><script>var arg1="ABC123";</script></html>',
|
||||
headers: {
|
||||
get: (name: string) => (name.toLowerCase() === 'content-type' ? 'text/html; charset=utf-8' : null),
|
||||
},
|
||||
});
|
||||
|
||||
const site = db.insert(schema.sites).values({
|
||||
name: 'AnyRouter Alias',
|
||||
url: 'https://anyrouter-alias.example.com',
|
||||
platform: 'newapi',
|
||||
}).returning().get();
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/accounts/verify-token',
|
||||
payload: {
|
||||
siteId: site.id,
|
||||
accessToken: 'session-or-cookie-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: false,
|
||||
message: 'Token invalid: cannot use it as session cookie or API key',
|
||||
});
|
||||
expect(response.json()).not.toMatchObject({
|
||||
shieldBlocked: true,
|
||||
});
|
||||
expect(undiciFetchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still returns shieldBlocked for non-new-api platforms when challenge html is detected', async () => {
|
||||
adapterPlatformName = 'one-api';
|
||||
verifyTokenMock.mockResolvedValueOnce({ tokenType: 'unknown' });
|
||||
undiciFetchMock.mockResolvedValue({
|
||||
text: async () => '<html><script>var arg1="ABC123";</script></html>',
|
||||
headers: {
|
||||
get: (name: string) => (name.toLowerCase() === 'content-type' ? 'text/html; charset=utf-8' : null),
|
||||
},
|
||||
});
|
||||
|
||||
const site = db.insert(schema.sites).values({
|
||||
name: 'Legacy Shielded',
|
||||
url: 'https://legacy-shield.example.com',
|
||||
platform: 'one-api',
|
||||
}).returning().get();
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/accounts/verify-token',
|
||||
payload: {
|
||||
siteId: site.id,
|
||||
accessToken: 'session-or-cookie-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: false,
|
||||
|
||||
Reference in New Issue
Block a user