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(); +});