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 }; }