From eca4a32d87db8da2a1b2aedc0729dff26303369d Mon Sep 17 00:00:00 2001 From: pacocartones Date: Mon, 24 Aug 2026 07:59:08 +0000 Subject: [PATCH] fix(fetch): do not end SSE stream on comment lines parseSseLine treated a `: ping` keep-alive as done: true, which breaks out of the line loop in streamSse. When a server or proxy flushes that comment in the same chunk as the data events that follow, those events are left in the buffer and the trailing-buffer fallback cannot parse them, so the rest of the model response is silently dropped. Per the SSE spec any line starting with a colon is a comment and must be ignored, so return done: false for it and keep draining the buffer. --- packages/fetch/src/stream.test.ts | 26 ++++++++++++++++++++++++++ packages/fetch/src/stream.ts | 5 +++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/fetch/src/stream.test.ts b/packages/fetch/src/stream.test.ts index 084002ac54a..c340780b17b 100644 --- a/packages/fetch/src/stream.test.ts +++ b/packages/fetch/src/stream.test.ts @@ -54,6 +54,32 @@ describe("streamSse", () => { expect(results).toEqual([{ foo: "bar" }, { baz: 42 }]); }); + it("ignores SSE comment lines that share a chunk with data events", async () => { + // Servers and proxies send SSE comments (e.g. `: ping`) as keep-alives. + // When one arrives in the same chunk as the following data events, those + // events must still be yielded rather than silently dropped. + const stream = new Readable({ + read() { + this.push( + ': ping\n\ndata: {"foo": "bar"}\n\ndata: {"baz": 42}\n\ndata: [DONE]\n\n', + ); + this.push(null); // End of stream + }, + }) as any; + const response = { + status: 200, + body: stream, + text: async () => "", + } as unknown as Response; + + const results = []; + for await (const data of streamSse(response)) { + results.push(data); + } + + expect(results).toEqual([{ foo: "bar" }, { baz: 42 }]); + }); + it("throws on malformed JSON", async () => { const sseLines = ['data: {"foo": "bar"', "data:[DONE]"]; const response = createMockResponse(sseLines); diff --git a/packages/fetch/src/stream.ts b/packages/fetch/src/stream.ts index f73d61dbafc..29f79ff682f 100644 --- a/packages/fetch/src/stream.ts +++ b/packages/fetch/src/stream.ts @@ -115,8 +115,9 @@ function parseSseLine(line: string): { done: boolean; data: any } { if (line.startsWith("data:")) { return { done: false, data: parseDataLine(line) }; } - if (line.startsWith(": ping")) { - return { done: true, data: undefined }; + if (line.startsWith(":")) { + // Comment line (e.g. the `: ping` keep-alive), ignored per the SSE spec + return { done: false, data: undefined }; } return { done: false, data: undefined }; }