diff --git a/src/error-classifier.ts b/src/error-classifier.ts index aeb2f6f..e864b9f 100644 --- a/src/error-classifier.ts +++ b/src/error-classifier.ts @@ -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).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], diff --git a/tests/error-classifier.test.ts b/tests/error-classifier.test.ts index 47d39b8..bbeb0a7 100644 --- a/tests/error-classifier.test.ts +++ b/tests/error-classifier.test.ts @@ -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(); + }); + }); }); diff --git a/tests/runtime-oauth-detection.test.ts b/tests/runtime-oauth-detection.test.ts index 2057001..ec9a2b3 100644 --- a/tests/runtime-oauth-detection.test.ts +++ b/tests/runtime-oauth-detection.test.ts @@ -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); + }); });