fix(server): call onerror callback for all transport errors (#1433)

Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
kenzo 2026-03-25 21:28:28 +01:00 • committed by GitHub
commit 78bae7426d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -765,4 +765,195 @@ describe('Zod v4', () => {
await expect(transport.start()).rejects.toThrow('Transport already started');
});
});
describe('HTTPServerTransport - onerror callback', () => {
let transport: WebStandardStreamableHTTPServerTransport;
let mcpServer: McpServer;
let errors: Error[];
beforeEach(async () => {
errors = [];
mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: {} });
transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
});
transport.onerror = err => errors.push(err);
await mcpServer.connect(transport);
});
afterEach(async () => {
await transport.close();
});
async function initializeServer(): Promise<string> {
const request = createRequest('POST', TEST_MESSAGES.initialize);
const response = await transport.handleRequest(request);
return response.headers.get('mcp-session-id') as string;
}
it('should call onerror for invalid JSON', async () => {
const request = new Request('http://localhost/mcp', {
method: 'POST',
headers: {
Accept: 'application/json, text/event-stream',
'Content-Type': 'application/json'
},
body: 'not valid json'
});
const response = await transport.handleRequest(request);
expect(response.status).toBe(400);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error).toBeInstanceOf(SyntaxError);
});
it('should call onerror for invalid JSON-RPC message', async () => {
const request = new Request('http://localhost/mcp', {
method: 'POST',
headers: {
Accept: 'application/json, text/event-stream',
'Content-Type': 'application/json'
},
body: JSON.stringify({ not: 'valid jsonrpc' })
});
const response = await transport.handleRequest(request);
expect(response.status).toBe(400);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.name).toBe('ZodError');
});
it('should call onerror for missing Accept header on POST', async () => {
const request = createRequest('POST', TEST_MESSAGES.initialize, { accept: 'application/json' });
const response = await transport.handleRequest(request);
expect(response.status).toBe(406);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Not Acceptable');
});
it('should call onerror for unsupported Content-Type', async () => {
const request = new Request('http://localhost/mcp', {
method: 'POST',
headers: {
Accept: 'application/json, text/event-stream',
'Content-Type': 'text/plain'
},
body: JSON.stringify(TEST_MESSAGES.initialize)
});
const response = await transport.handleRequest(request);
expect(response.status).toBe(415);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Unsupported Media Type');
});
it('should call onerror for server not initialized', async () => {
const request = createRequest('POST', TEST_MESSAGES.toolsList);
const response = await transport.handleRequest(request);
expect(response.status).toBe(400);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Server not initialized');
});
it('should call onerror for invalid session ID', async () => {
await initializeServer();
const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId: 'invalid-session-id' });
const response = await transport.handleRequest(request);
expect(response.status).toBe(404);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Session not found');
});
it('should call onerror for re-initialization attempt', async () => {
await initializeServer();
const request = createRequest('POST', TEST_MESSAGES.initialize);
const response = await transport.handleRequest(request);
expect(response.status).toBe(400);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Server already initialized');
});
it('should call onerror for GET without Accept header', async () => {
const sessionId = await initializeServer();
const request = createRequest('GET', undefined, { sessionId, accept: 'application/json' });
const response = await transport.handleRequest(request);
expect(response.status).toBe(406);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Not Acceptable');
});
it('should call onerror for concurrent SSE streams', async () => {
const sessionId = await initializeServer();
const request1 = createRequest('GET', undefined, { sessionId });
await transport.handleRequest(request1);
const request2 = createRequest('GET', undefined, { sessionId });
const response2 = await transport.handleRequest(request2);
expect(response2.status).toBe(409);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Conflict');
});
it('should call onerror for unsupported protocol version', async () => {
const sessionId = await initializeServer();
const request = new Request('http://localhost/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
'mcp-session-id': sessionId,
'mcp-protocol-version': 'unsupported-version'
},
body: JSON.stringify(TEST_MESSAGES.toolsList)
});
const response = await transport.handleRequest(request);
expect(response.status).toBe(400);
expect(errors.length).toBeGreaterThan(0);
const error = errors[0];
expect(error).toBeDefined();
expect(error?.message).toContain('Unsupported protocol version');
});
});
});