Compare commits
2 Commits
main
...
fix/error-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a02dda1b44 | ||
|
|
049f3b7d6b |
@ -3,6 +3,7 @@
|
||||
## [Unreleased]
|
||||
|
||||
### CLI
|
||||
- Detect auth failures from `error.code` metadata too, so Streamable HTTP/SSE transports still trigger OAuth correctly when messages omit the numeric status. (PR #94, thanks @KentonYu)
|
||||
- Preserve default imports when `mcporter config add` writes a config file, instead of forcing `"imports": []`.
|
||||
- OAuth: avoid crashing on headless Linux when `xdg-open` is unavailable; clear stale dynamic-port client registrations; close callback server if stale-client persistence reads fail. (PR #72, thanks @mgonto)
|
||||
- Added optional `oauthScope`/`oauth_scope` config override as an escape hatch for providers that require explicit scopes.
|
||||
|
||||
@ -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,22 @@ 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;
|
||||
}
|
||||
if (typeof code === 'string') {
|
||||
const parsed = Number.parseInt(code, 10);
|
||||
if (Number.isFinite(parsed) && parsed >= 100 && parsed < 600) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractStatusCode(message: string): number | undefined {
|
||||
const candidates = [
|
||||
message.match(/status code\s*\((\d{3})\)/i)?.[1],
|
||||
|
||||
@ -43,4 +43,54 @@ 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('accepts numeric-string HTTP codes from SDK errors', () => {
|
||||
const err = Object.assign(new Error('Forbidden'), { code: '403' });
|
||||
const issue = analyzeConnectionError(err);
|
||||
expect(issue.kind).toBe('auth');
|
||||
expect(issue.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('ignores non-http string codes like errno values', () => {
|
||||
const err = Object.assign(new Error('spawn enoent'), { code: 'ENOENT' });
|
||||
const issue = analyzeConnectionError(err);
|
||||
expect(issue.kind).toBe('offline');
|
||||
expect(issue.statusCode).toBeUndefined();
|
||||
});
|
||||
|
||||
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,14 @@ 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);
|
||||
});
|
||||
|
||||
it('matches errors with string code=401 too', () => {
|
||||
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