fix: detect auth errors from error.code property (StreamableHTTPError/SseError)
MCP SDK's StreamableHTTPError/SseError store the HTTP status code in error.code, but the message text may not contain the numeric status. analyzeConnectionError now reads error.code (100-599) before falling back to message-text parsing, so OAuth promotion triggers correctly for 401 responses on Streamable HTTP transport.
This commit is contained in:
parent
8af1a77c45
commit
049f3b7d6b
@ -47,7 +47,8 @@ export function analyzeConnectionError(error: unknown): ConnectionIssue {
|
||||
if (stdio) {
|
||||
return { kind: 'stdio-exit', rawMessage, ...stdio };
|
||||
}
|
||||
const statusCode = extractStatusCode(rawMessage);
|
||||
const errorCode = extractErrorCode(error);
|
||||
const statusCode = errorCode ?? extractStatusCode(rawMessage);
|
||||
const normalized = rawMessage.toLowerCase();
|
||||
if (AUTH_STATUSES.has(statusCode ?? -1) || containsAuthToken(normalized)) {
|
||||
return { kind: 'auth', rawMessage, statusCode };
|
||||
@ -82,6 +83,16 @@ function extractMessage(error: unknown): string {
|
||||
}
|
||||
}
|
||||
|
||||
function extractErrorCode(error: unknown): number | undefined {
|
||||
if (typeof error === 'object' && error !== null && 'code' in error) {
|
||||
const code = (error as Record<string, unknown>).code;
|
||||
if (typeof code === 'number' && Number.isFinite(code) && code >= 100 && code < 600) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractStatusCode(message: string): number | undefined {
|
||||
const candidates = [
|
||||
message.match(/status code\s*\((\d{3})\)/i)?.[1],
|
||||
|
||||
@ -43,4 +43,40 @@ describe('analyzeConnectionError', () => {
|
||||
expect(issue.kind).toBe('http');
|
||||
expect(issue.statusCode).toBe(503);
|
||||
});
|
||||
|
||||
describe('error.code property (StreamableHTTPError / SseError)', () => {
|
||||
it('classifies code=401 as auth even when message lacks 401', () => {
|
||||
const err = Object.assign(new Error('Error POSTing to endpoint: {}'), { code: 401 });
|
||||
const issue = analyzeConnectionError(err);
|
||||
expect(issue.kind).toBe('auth');
|
||||
expect(issue.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('classifies code=403 as auth', () => {
|
||||
const err = Object.assign(new Error('Forbidden'), { code: 403 });
|
||||
const issue = analyzeConnectionError(err);
|
||||
expect(issue.kind).toBe('auth');
|
||||
expect(issue.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('classifies code=404 as http (not auth)', () => {
|
||||
const err = Object.assign(new Error('Not Found'), { code: 404 });
|
||||
const issue = analyzeConnectionError(err);
|
||||
expect(issue.kind).toBe('http');
|
||||
expect(issue.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('classifies code=500 as http', () => {
|
||||
const err = Object.assign(new Error('Internal Server Error'), { code: 500 });
|
||||
const issue = analyzeConnectionError(err);
|
||||
expect(issue.kind).toBe('http');
|
||||
expect(issue.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
it('falls back to message parsing when code is absent', () => {
|
||||
const issue = analyzeConnectionError(new Error('network timeout'));
|
||||
expect(issue.kind).toBe('offline');
|
||||
expect(issue.statusCode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -69,4 +69,9 @@ describe('isUnauthorizedError helper', () => {
|
||||
it('ignores unrelated errors', () => {
|
||||
expect(isUnauthorizedError(new Error('network timeout'))).toBe(false);
|
||||
});
|
||||
|
||||
it('matches errors with code=401 even when message lacks 401', () => {
|
||||
const err = Object.assign(new Error('Error POSTing to endpoint: {}'), { code: 401 });
|
||||
expect(isUnauthorizedError(err)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user