fix(mcp): suppress unhandled error on stderr pipe in stdio transport (#99803)

* fix(mcp): suppress unhandled error on stderr pipe in stdio transport

When child.stderr is piped to stderrStream without an error
handler, a stream-level error (EPIPE, I/O failure) crashes the
process. Add a noop error handler before the pipe, consistent
with the error handlers already present on stdin and stdout.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(mcp): add regression test for stderr pipe error suppression

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mcp): report stderr stream errors

* fix(mcp): report stderr stream errors

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
cxbAsDev
2026-07-04 13:42:12 +08:00
committed by GitHub
parent acdaf8ae31
commit 1b84316a91
2 changed files with 20 additions and 0 deletions
+19
View File
@@ -204,4 +204,23 @@ describe("OpenClawStdioClientTransport", () => {
"write after end",
);
});
it("reports stderr pipe errors without an unhandled error crash", async () => {
const child = new MockChildProcess();
spawnMock.mockReturnValue(child);
const transport = new OpenClawStdioClientTransport({ command: "npx", stderr: "pipe" });
const onerror = vi.fn();
Object.assign(transport, { onerror });
const started = transport.start();
child.emit("spawn");
await started;
const error = new Error("simulated pipe error");
expect(() => child.stderr?.emit("error", error)).not.toThrow();
expect(onerror).toHaveBeenCalledWith(error);
child.stderr.write("server diagnostic");
expect(transport.stderr?.read()?.toString()).toBe("server diagnostic");
});
});
+1
View File
@@ -86,6 +86,7 @@ export class OpenClawStdioClientTransport implements Transport {
});
child.stdout?.on("error", (error: Error) => this.onerror?.(error));
if (this.stderrStream && child.stderr) {
child.stderr.on("error", (error: Error) => this.onerror?.(error));
child.stderr.pipe(this.stderrStream);
}
});