mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +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 eventCount: number;
|
||||||
private maxBufferSize: number;
|
private maxBufferSize: number;
|
||||||
private throwOnMalformedJson: boolean;
|
private throwOnMalformedJson: boolean;
|
||||||
|
private pendingCR: boolean;
|
||||||
|
|
||||||
constructor(options: SSEParserOptions = {}) {
|
constructor(options: SSEParserOptions = {}) {
|
||||||
this.buffer = '';
|
this.buffer = '';
|
||||||
this.eventCount = 0;
|
this.eventCount = 0;
|
||||||
this.maxBufferSize = options.maxBufferSize || 1024 * 1024; // 1MB default
|
this.maxBufferSize = options.maxBufferSize || 1024 * 1024; // 1MB default
|
||||||
this.throwOnMalformedJson = options.throwOnMalformedJson === true;
|
this.throwOnMalformedJson = options.throwOnMalformedJson === true;
|
||||||
|
this.pendingCR = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +51,19 @@ export class SSEParser {
|
|||||||
* @returns Array of parsed events
|
* @returns Array of parsed events
|
||||||
*/
|
*/
|
||||||
parse(chunk: Buffer | string): SSEEvent[] {
|
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)
|
// C-01 Fix: Prevent unbounded buffer growth (DoS protection)
|
||||||
if (this.buffer.length > this.maxBufferSize) {
|
if (this.buffer.length > this.maxBufferSize) {
|
||||||
@@ -127,5 +141,6 @@ export class SSEParser {
|
|||||||
reset(): void {
|
reset(): void {
|
||||||
this.buffer = '';
|
this.buffer = '';
|
||||||
this.eventCount = 0;
|
this.eventCount = 0;
|
||||||
|
this.pendingCR = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,4 +46,18 @@ describe('SSEParser', () => {
|
|||||||
?.delta?.content
|
?.delta?.content
|
||||||
).toBe('Legacy');
|
).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