From 647c39b3ade7311809c6e5a8b8ed39cd7ecf4656 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Sat, 12 Sep 2026 22:08:30 -0700 Subject: [PATCH] fix: prevent stderr pipe deadlock in StdioClientTransport (fixes #2776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When stderr: 'pipe' is used and nobody attaches a reader, the pipe fills up, the child process blocks on write(2), and the session silently hangs — no error, no rejection, no timeout. Fixed by calling .resume() on the stderr PassThrough stream after piping, so it flows into the internal buffer and never blocks the child. A consumer that attaches a listener still gets the data; one that does not gets a working session instead of a hang. --- src/client/stdio.ts | 5 +++++ test/client/stdio.test.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/client/stdio.ts b/src/client/stdio.ts index 816a686fce..cd0aadfad8 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -172,6 +172,11 @@ export class StdioClientTransport implements Transport { if (this._stderrStream && this._process.stderr) { this._process.stderr.pipe(this._stderrStream); + // Auto-resume so the pipe never blocks the child process. + // Without this, a server that logs to stderr will deadlock if + // nobody attaches a reader — the pipe fills, the child blocks + // on write(2), and the session silently hangs. + this._stderrStream.resume(); } }); } diff --git a/test/client/stdio.test.ts b/test/client/stdio.test.ts index 577e0c946f..c92c4f6957 100644 --- a/test/client/stdio.test.ts +++ b/test/client/stdio.test.ts @@ -116,3 +116,34 @@ test('should fire onerror and close when ReadBuffer overflows', async () => { expect(error.message).toMatch(/ReadBuffer exceeded maximum size/); await closed; }); + +test('stderr pipe should not deadlock when nobody reads it', async () => { + // Spawn a server that writes a lot to stderr. + // Without auto-resume, the pipe fills, the child blocks on write(2), + // and the session hangs silently. + const client = new StdioClientTransport({ + command: 'node', + args: ['-e', ` + process.stderr.write('log line '.repeat(5000) + '\\n'); + process.stdout.write(JSON.stringify({jsonrpc:'2.0', id:1, result:{}}) + '\\n'); + `], + stderr: 'pipe' + }); + + // Do NOT attach a reader to client.stderr — this is the bug scenario. + const started = Date.now(); + await Promise.race([ + client.start().then(() => { + return new Promise((resolve) => { + client.onmessage = () => resolve(); + }); + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Session hung - likely stderr deadlock')), 5000) + ) + ]); + + // Should complete quickly, not hang + expect(Date.now() - started).toBeLessThan(5000); + await client.close(); +});