From 36a67c48377a0fbd325d2ed9a267de5c5357b52f Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Fri, 13 Feb 2026 05:33:56 +0700 Subject: [PATCH] fix(cursor): close ReadableStream controller on abort to prevent consumer hangs When abort fires after streaming begins, onAbort closed HTTP/2 handles but never closed the ReadableStream controller. Consumers waiting on reader.read() could hang indefinitely since end/error handlers were gated by streamClosed flag. Hoist controller reference to outer scope so abort handler can close it. --- src/cursor/cursor-executor.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cursor/cursor-executor.ts b/src/cursor/cursor-executor.ts index c404d1a9..43ec4d5e 100644 --- a/src/cursor/cursor-executor.ts +++ b/src/cursor/cursor-executor.ts @@ -416,9 +416,18 @@ export class CursorExecutor { // Register abort before response headers arrive so cancellation works at all stages let streamClosed = false; + let streamController: ReadableStreamDefaultController | null = null; if (signal) { const onAbort = () => { streamClosed = true; + // Close the ReadableStream controller so consumers don't hang on reader.read() + if (streamController) { + try { + streamController.close(); + } catch { + /* already closed */ + } + } req.close(); client.close(); }; @@ -471,6 +480,8 @@ export class CursorExecutor { const readable = new ReadableStream({ start(controller) { + streamController = controller; + const emitSSE = (data: string) => { if (streamClosed) return; controller.enqueue(enc.encode(`data: ${data}\n\n`));