🛂 fix: Detect OAuth Errors From HTTP 400 Responses (#11961)

* fix(mcp): detect non-standard OAuth errors from servers returning HTTP 400

* add tests for oauth error check

* fix(mcp): align factory OAuth error detection
This commit is contained in:
janluedemann-esome 2026-05-23 15:09:13 +02:00 committed by GitHub
parent fb851cae63
commit abda15f4eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 100 additions and 0 deletions

View file

@ -626,10 +626,18 @@ export class MCPConnectionFactory {
if (message.includes('invalid_token')) {
return true;
}
// Check for invalid_grant (OAuth servers return this for expired/revoked grants)
if (message.includes('invalid_grant')) {
return true;
}
// Check for authentication required
if (message.includes('authentication required') || message.includes('unauthorized')) {
return true;
}
// Check for missing authorization values (e.g., Amazon Ads MCP returns HTTP 400 with this)
if (message.includes('no authorization')) {
return true;
}
}
return false;

View file

@ -82,6 +82,10 @@ describe('MCPConnection Error Detection', () => {
if (message.includes('authentication required') || message.includes('unauthorized')) {
return true;
}
// Check for missing authorization values (e.g., Amazon Ads MCP returns HTTP 400 with this)
if (message.includes('no authorization')) {
return true;
}
}
return false;
@ -171,6 +175,24 @@ describe('MCPConnection Error Detection', () => {
};
expect(isOAuthError(error)).toBe(true);
});
it('should detect OAuth error for "no authorization" in message (HTTP 400)', () => {
const error = {
message:
'Either no authorization values are specified or it could not be derived from the request',
};
expect(isOAuthError(error)).toBe(true);
});
it('should detect OAuth error for "No authorization" with different casing', () => {
const error = { message: 'No Authorization header provided' };
expect(isOAuthError(error)).toBe(true);
});
it('should not detect OAuth error for unrelated 400 errors', () => {
const error = { code: 400, message: 'Bad request: missing required field' };
expect(isOAuthError(error)).toBe(false);
});
});
describe('error type differentiation', () => {

View file

@ -844,6 +844,72 @@ describe('MCPConnectionFactory', () => {
expect.stringContaining('OAuth required, stopping connection attempts'),
);
});
it('should identify "no authorization" errors as OAuth errors (HTTP 400)', async () => {
const basicOptions = {
serverName: 'test-server',
serverConfig: mockServerConfig,
};
const oauthOptions = {
useOAuth: true as const,
user: mockUser,
flowManager: mockFlowManager,
tokenMethods: {
findToken: jest.fn(),
createToken: jest.fn(),
updateToken: jest.fn(),
deleteTokens: jest.fn(),
},
};
const noAuthError = new Error(
'Either no authorization values are specified or it could not be derived from the request',
);
mockConnectionInstance.connect.mockRejectedValue(noAuthError);
mockConnectionInstance.isConnected.mockResolvedValue(false);
await expect(MCPConnectionFactory.create(basicOptions, oauthOptions)).rejects.toThrow(
'no authorization',
);
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringContaining('OAuth required, stopping connection attempts'),
);
});
it('should identify invalid_grant errors as OAuth errors', async () => {
const basicOptions = {
serverName: 'test-server',
serverConfig: mockServerConfig,
};
const oauthOptions = {
useOAuth: true as const,
user: mockUser,
flowManager: mockFlowManager,
tokenMethods: {
findToken: jest.fn(),
createToken: jest.fn(),
updateToken: jest.fn(),
deleteTokens: jest.fn(),
},
};
const invalidGrantError = new Error(
'Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_grant"}',
);
mockConnectionInstance.connect.mockRejectedValue(invalidGrantError);
mockConnectionInstance.isConnected.mockResolvedValue(false);
await expect(MCPConnectionFactory.create(basicOptions, oauthOptions)).rejects.toThrow(
'invalid_grant',
);
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringContaining('OAuth required, stopping connection attempts'),
);
});
});
describe('discoverTools static method', () => {

View file

@ -2326,6 +2326,10 @@ export class MCPConnection extends EventEmitter {
if (message.includes('authentication required') || message.includes('unauthorized')) {
return true;
}
// Check for missing authorization values (e.g., Amazon Ads MCP returns HTTP 400 with this)
if (message.includes('no authorization')) {
return true;
}
}
return false;