Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/client/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,57 @@ describe('StreamableHTTPClientTransport', () => {
const reconnectHeaders = fetchMock.mock.calls[1][1]?.headers as Headers;
expect(reconnectHeaders.get('last-event-id')).toBe('event-123');
});

it('should not throw JSON parse error on priming events with empty data', async () => {
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'));

const errorSpy = vi.fn();
transport.onerror = errorSpy;

const resumptionTokenSpy = vi.fn();

// Create a stream that sends a priming event (ID only, empty data) then a real message
const streamWithPrimingEvent = new ReadableStream({
start(controller) {
// Send a priming event with ID but empty data - this should NOT cause a JSON parse error
controller.enqueue(new TextEncoder().encode('id: priming-123\ndata: \n\n'));
// Send a real message
controller.enqueue(
new TextEncoder().encode('id: msg-456\ndata: {"jsonrpc":"2.0","result":{"tools":[]},"id":"req-1"}\n\n')
);
controller.close();
}
});

const fetchMock = global.fetch as Mock;
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'text/event-stream' }),
body: streamWithPrimingEvent
});

await transport.start();
transport.send(
{
jsonrpc: '2.0',
method: 'tools/list',
id: 'req-1',
params: {}
},
{ resumptionToken: undefined, onresumptiontoken: resumptionTokenSpy }
);

await vi.advanceTimersByTimeAsync(50);

// No JSON parse errors should have occurred
expect(errorSpy).not.toHaveBeenCalledWith(
expect.objectContaining({ message: expect.stringContaining('Unexpected end of JSON') })
);
// Resumption token callback should have been called for both events with IDs
expect(resumptionTokenSpy).toHaveBeenCalledWith('priming-123');
expect(resumptionTokenSpy).toHaveBeenCalledWith('msg-456');
});
});

it('invalidates all credentials on InvalidClientError during auth', async () => {
Expand Down
5 changes: 5 additions & 0 deletions src/client/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ export class StreamableHTTPClientTransport implements Transport {
onresumptiontoken?.(event.id);
}

// Skip events with no data (priming events, keep-alives)
if (!event.data) {
continue;
}

if (!event.event || event.event === 'message') {
try {
const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));
Expand Down
Loading