diff --git a/src/vs/base/common/sseParser.ts b/src/vs/base/common/sseParser.ts index 3648918d971..0990e0247d9 100644 --- a/src/vs/base/common/sseParser.ts +++ b/src/vs/base/common/sseParser.ts @@ -55,6 +55,7 @@ export class SSEParser { private lastEventIdBuffer?: string; private reconnectionTime?: number; private buffer: Uint8Array[] = []; + private endedOnCR = false; private readonly onEventHandler: SSEEventHandler; private readonly decoder: TextDecoder; /** @@ -84,8 +85,18 @@ export class SSEParser { * @param chunk The chunk to parse as a Uint8Array of UTF-8 encoded data. */ public feed(chunk: Uint8Array): void { + if (chunk.length === 0) { + return; + } + let offset = 0; + // If the data stream was bifurcated between a CR and LF, avoid processing the CR as an extra newline + if (this.endedOnCR && chunk[0] === Chr.LF) { + offset++; + } + this.endedOnCR = false; + // Process complete lines from the buffer while (offset < chunk.length) { const indexCR = chunk.indexOf(Chr.CR, offset); @@ -103,12 +114,14 @@ export class SSEParser { this.processLine(str); this.buffer.length = 0; - offset = index + (chunk[offset] === Chr.CR && chunk[offset + 1] === Chr.LF ? 2 : 1); + offset = index + (chunk[index] === Chr.CR && chunk[index + 1] === Chr.LF ? 2 : 1); } if (offset < chunk.length) { this.buffer.push(chunk.subarray(offset)); + } else { + this.endedOnCR = chunk[chunk.length - 1] === Chr.CR; } } /** diff --git a/src/vs/base/test/common/sseParser.test.ts b/src/vs/base/test/common/sseParser.test.ts index 8c84fefd5de..c87664b6fb1 100644 --- a/src/vs/base/test/common/sseParser.test.ts +++ b/src/vs/base/test/common/sseParser.test.ts @@ -42,6 +42,31 @@ suite('SSEParser', () => { assert.strictEqual(receivedEvents[0].type, 'custom'); assert.strictEqual(receivedEvents[0].data, 'hello world'); }); + test('handles events with explicit event type (CRLF)', () => { + parser.feed(toUint8Array('event: custom\r\ndata: hello world\r\n\r\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello world'); + }); + test('stream processing chunks', () => { + for (const lf of ['\n', '\r\n', '\r']) { + const message = toUint8Array(`event: custom${lf}data: hello world${lf}${lf}event: custom2${lf}data: hello world2${lf}${lf}`); + for (let chunkSize = 1; chunkSize < 5; chunkSize++) { + receivedEvents.length = 0; + + for (let i = 0; i < message.length; i += chunkSize) { + const chunk = message.slice(i, i + chunkSize); + parser.feed(chunk); + } + + assert.deepStrictEqual(receivedEvents, [ + { type: 'custom', data: 'hello world' }, + { type: 'custom2', data: 'hello world2' } + ], `Failed for chunk size ${chunkSize} and line ending ${JSON.stringify(lf)}`); + } + } + }); test('handles events with ID', () => { parser.feed(toUint8Array('event: custom\ndata: hello\nid: 123\n\n'));