mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
fix(sse): make CRLF normalization chunk-boundary safe (#1359)
* fix(sse): handle CRLF split across chunk boundaries * style: apply prettier formatting
This commit is contained in:
+16
-1
@@ -35,12 +35,14 @@ export class SSEParser {
|
||||
private eventCount: number;
|
||||
private maxBufferSize: number;
|
||||
private throwOnMalformedJson: boolean;
|
||||
private pendingCR: boolean;
|
||||
|
||||
constructor(options: SSEParserOptions = {}) {
|
||||
this.buffer = '';
|
||||
this.eventCount = 0;
|
||||
this.maxBufferSize = options.maxBufferSize || 1024 * 1024; // 1MB default
|
||||
this.throwOnMalformedJson = options.throwOnMalformedJson === true;
|
||||
this.pendingCR = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +51,19 @@ export class SSEParser {
|
||||
* @returns Array of parsed events
|
||||
*/
|
||||
parse(chunk: Buffer | string): SSEEvent[] {
|
||||
this.buffer += chunk.toString().replace(/\r\n?/g, '\n');
|
||||
let normalizedChunk = chunk.toString();
|
||||
|
||||
if (this.pendingCR) {
|
||||
if (normalizedChunk.startsWith('\n')) {
|
||||
normalizedChunk = normalizedChunk.slice(1);
|
||||
}
|
||||
this.pendingCR = false;
|
||||
}
|
||||
|
||||
const endsWithCR = normalizedChunk.endsWith('\r');
|
||||
normalizedChunk = normalizedChunk.replace(/\r\n?/g, '\n');
|
||||
this.pendingCR = endsWithCR;
|
||||
this.buffer += normalizedChunk;
|
||||
|
||||
// C-01 Fix: Prevent unbounded buffer growth (DoS protection)
|
||||
if (this.buffer.length > this.maxBufferSize) {
|
||||
@@ -127,5 +141,6 @@ export class SSEParser {
|
||||
reset(): void {
|
||||
this.buffer = '';
|
||||
this.eventCount = 0;
|
||||
this.pendingCR = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,4 +46,18 @@ describe('SSEParser', () => {
|
||||
?.delta?.content
|
||||
).toBe('Legacy');
|
||||
});
|
||||
|
||||
|
||||
it('does not split events when CRLF is split across chunks', () => {
|
||||
const parser = new SSEParser({ throwOnMalformedJson: true });
|
||||
|
||||
expect(parser.parse('data: {"choices":[\r')).toEqual([]);
|
||||
const events = parser.parse('\ndata: {"delta":{"content":"Hello"}}\r\ndata: ]}\r\n\r\n');
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(
|
||||
(events[0]?.data as { choices?: Array<{ delta?: { content?: string } }> })?.choices?.[0]
|
||||
?.delta?.content
|
||||
).toBe('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user