From 32cc9b08e6b3d6d7f297bdf1008b6fffc4e00b2b Mon Sep 17 00:00:00 2001 From: cxbAsDev Date: Tue, 7 Jul 2026 00:03:29 +0800 Subject: [PATCH] fix(ssh-tunnel): ignore stderr stream errors during teardown (#100855) * fix(ssh-tunnel): ignore stderr stream errors during teardown * chore(proof): add real behavior proof for ssh-tunnel stderr stream errors * chore(proof): fix promise executor lint in ssh-tunnel proof * test(ssh-tunnel): keep stream error proof in regression suite * test(ssh): cover active stream errors --------- Co-authored-by: Vincent Koc Co-authored-by: Peter Steinberger --- src/infra/ssh-tunnel.test.ts | 25 +++++++++++++++++++++++++ src/infra/ssh-tunnel.ts | 8 ++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/infra/ssh-tunnel.test.ts b/src/infra/ssh-tunnel.test.ts index 608be32858b6..9a16d09941f3 100644 --- a/src/infra/ssh-tunnel.test.ts +++ b/src/infra/ssh-tunnel.test.ts @@ -205,4 +205,29 @@ describe("startSshPortForward", () => { await rejection; expect(kill).toHaveBeenCalledWith("SIGTERM"); }); + + it.each(["active", "teardown"] as const)( + "does not crash when stderr errors while the tunnel is %s", + async (phase) => { + vi.useFakeTimers(); + spawnFakeSshListening(); + + const tunnel = await startSshPortForward({ + target: "me@example.com:2222", + localPortPreferred: 43210, + remotePort: 18789, + timeoutMs: 1000, + }); + + const child = mocks.spawn.mock.results[0]?.value as EventEmitter & { + killed: boolean; + stderr: EventEmitter; + }; + const stopping = phase === "teardown" ? tunnel.stop() : undefined; + expect(child.killed).toBe(phase === "teardown"); + expect(() => child.stderr.emit("error", new Error("stderr EPIPE"))).not.toThrow(); + + await expect(stopping ?? tunnel.stop()).resolves.toBeUndefined(); + }, + ); }); diff --git a/src/infra/ssh-tunnel.ts b/src/infra/ssh-tunnel.ts index 94d973e642e0..d9ed2908cee0 100644 --- a/src/infra/ssh-tunnel.ts +++ b/src/infra/ssh-tunnel.ts @@ -165,8 +165,12 @@ export async function startSshPortForward(opts: { const child = spawn("/usr/bin/ssh", args, { stdio: ["ignore", "ignore", "pipe"], }); - child.stderr?.setEncoding("utf8"); - child.stderr?.on("data", (chunk) => { + const stderrStream = child.stderr; + // Child events own tunnel failure. Keep the diagnostic pipe observed so a + // stream error cannot become an uncaught exception during active use or teardown. + stderrStream?.on("error", () => {}); + stderrStream?.setEncoding("utf8"); + stderrStream?.on("data", (chunk) => { const lines = normalizeStringEntries(String(chunk).split("\n")); stderr.push(...lines); });