Skip to content

Commit 48d5606

Browse files
committed
Add test for error response handling in SSE stream
Verify that error responses (not just success responses) also: - Set receivedResponse flag to prevent reconnection - Get delivered to the message handler correctly
1 parent 1593929 commit 48d5606

1 file changed

Lines changed: 72 additions & 0 deletions

File tree

packages/client/test/client/streamableHttp.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,78 @@ describe('StreamableHTTPClientTransport', () => {
944944
expect(fetchMock.mock.calls[0]![1]?.method).toBe('POST');
945945
});
946946

947+
it('should NOT reconnect a POST stream when error response was received', async () => {
948+
// ARRANGE
949+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
950+
reconnectionOptions: {
951+
initialReconnectionDelay: 10,
952+
maxRetries: 1,
953+
maxReconnectionDelay: 1000,
954+
reconnectionDelayGrowFactor: 1
955+
}
956+
});
957+
958+
const messageSpy = vi.fn();
959+
transport.onmessage = messageSpy;
960+
961+
// Create a stream that sends:
962+
// 1. Priming event with ID (enables potential reconnection)
963+
// 2. An error response (should also prevent reconnection, just like success)
964+
// 3. Then closes
965+
const streamWithErrorResponse = new ReadableStream({
966+
start(controller) {
967+
// Priming event with ID
968+
controller.enqueue(new TextEncoder().encode('id: priming-123\ndata: \n\n'));
969+
// An error response to the request (tool not found, for example)
970+
controller.enqueue(
971+
new TextEncoder().encode(
972+
'id: error-456\ndata: {"jsonrpc":"2.0","error":{"code":-32602,"message":"Tool not found"},"id":"request-1"}\n\n'
973+
)
974+
);
975+
// Stream closes normally
976+
controller.close();
977+
}
978+
});
979+
980+
const fetchMock = global.fetch as Mock;
981+
fetchMock.mockResolvedValueOnce({
982+
ok: true,
983+
status: 200,
984+
headers: new Headers({ 'content-type': 'text/event-stream' }),
985+
body: streamWithErrorResponse
986+
});
987+
988+
const requestMessage: JSONRPCRequest = {
989+
jsonrpc: '2.0',
990+
method: 'tools/call',
991+
id: 'request-1',
992+
params: { name: 'nonexistent-tool' }
993+
};
994+
995+
// ACT
996+
await transport.start();
997+
await transport.send(requestMessage);
998+
await vi.advanceTimersByTimeAsync(50);
999+
1000+
// ASSERT
1001+
// THE KEY ASSERTION: Fetch was called ONCE only - no reconnection!
1002+
// The error response was received, so no need to reconnect.
1003+
expect(fetchMock).toHaveBeenCalledTimes(1);
1004+
expect(fetchMock.mock.calls[0]![1]?.method).toBe('POST');
1005+
1006+
// Verify the error response was delivered to the message handler
1007+
expect(messageSpy).toHaveBeenCalledWith(
1008+
expect.objectContaining({
1009+
jsonrpc: '2.0',
1010+
error: expect.objectContaining({
1011+
code: -32602,
1012+
message: 'Tool not found'
1013+
}),
1014+
id: 'request-1'
1015+
})
1016+
);
1017+
});
1018+
9471019
it('should not attempt reconnection after close() is called', async () => {
9481020
// ARRANGE
9491021
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {

0 commit comments

Comments
 (0)