Skip to content

Commit 9203091

Browse files
Fix JSON parse error on SSE events with empty data (#1184)
1 parent b9538a2 commit 9203091

File tree

2 files changed

+56
-0
lines changed

2 files changed

+56
-0
lines changed

src/client/streamableHttp.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,57 @@ describe('StreamableHTTPClientTransport', () => {
865865
const reconnectHeaders = fetchMock.mock.calls[1][1]?.headers as Headers;
866866
expect(reconnectHeaders.get('last-event-id')).toBe('event-123');
867867
});
868+
869+
it('should not throw JSON parse error on priming events with empty data', async () => {
870+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'));
871+
872+
const errorSpy = vi.fn();
873+
transport.onerror = errorSpy;
874+
875+
const resumptionTokenSpy = vi.fn();
876+
877+
// Create a stream that sends a priming event (ID only, empty data) then a real message
878+
const streamWithPrimingEvent = new ReadableStream({
879+
start(controller) {
880+
// Send a priming event with ID but empty data - this should NOT cause a JSON parse error
881+
controller.enqueue(new TextEncoder().encode('id: priming-123\ndata: \n\n'));
882+
// Send a real message
883+
controller.enqueue(
884+
new TextEncoder().encode('id: msg-456\ndata: {"jsonrpc":"2.0","result":{"tools":[]},"id":"req-1"}\n\n')
885+
);
886+
controller.close();
887+
}
888+
});
889+
890+
const fetchMock = global.fetch as Mock;
891+
fetchMock.mockResolvedValueOnce({
892+
ok: true,
893+
status: 200,
894+
headers: new Headers({ 'content-type': 'text/event-stream' }),
895+
body: streamWithPrimingEvent
896+
});
897+
898+
await transport.start();
899+
transport.send(
900+
{
901+
jsonrpc: '2.0',
902+
method: 'tools/list',
903+
id: 'req-1',
904+
params: {}
905+
},
906+
{ resumptionToken: undefined, onresumptiontoken: resumptionTokenSpy }
907+
);
908+
909+
await vi.advanceTimersByTimeAsync(50);
910+
911+
// No JSON parse errors should have occurred
912+
expect(errorSpy).not.toHaveBeenCalledWith(
913+
expect.objectContaining({ message: expect.stringContaining('Unexpected end of JSON') })
914+
);
915+
// Resumption token callback should have been called for both events with IDs
916+
expect(resumptionTokenSpy).toHaveBeenCalledWith('priming-123');
917+
expect(resumptionTokenSpy).toHaveBeenCalledWith('msg-456');
918+
});
868919
});
869920

870921
it('invalidates all credentials on InvalidClientError during auth', async () => {

src/client/streamableHttp.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ export class StreamableHTTPClientTransport implements Transport {
338338
onresumptiontoken?.(event.id);
339339
}
340340

341+
// Skip events with no data (priming events, keep-alives)
342+
if (!event.data) {
343+
continue;
344+
}
345+
341346
if (!event.event || event.event === 'message') {
342347
try {
343348
const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));

0 commit comments

Comments
 (0)