From 3cea1dfea872f53946ccc67b8a2dad17155d52b7 Mon Sep 17 00:00:00 2001 From: Michel Thomazo Date: Tue, 7 Apr 2026 21:48:50 +0200 Subject: [PATCH 1/3] fix: propagate input stream errors through ndJsonStream The readable stream returned by ndJsonStream used try/finally, which swallowed errors from the underlying input stream. When the input errored (e.g. a child process crash), the readable stream would close cleanly instead of erroring, causing the Connection to treat it as a normal shutdown rather than propagating the error to pending requests. Changed to try/catch/finally so that input stream errors are forwarded via controller.error(), allowing Connection.#receive() to capture the error and reject all pending requests with the original error message. Added two tests: - Verifies ndJsonStream propagates immediate input stream errors - Verifies pending requests are rejected when input stream errors Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- package-lock.json | 37 ------------------------------------- src/acp.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/stream.ts | 5 ++++- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6266bf4..6019adf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,43 +31,6 @@ "zod": "^3.25.0 || ^4.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", diff --git a/src/acp.test.ts b/src/acp.test.ts index c76bcb4..c8ee09c 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -1013,6 +1013,51 @@ describe("Connection", () => { } } + it("propagates input stream errors through ndJsonStream", async () => { + const inputStream = new ReadableStream({ + start(controller) { + // Simulate a process crash after partial data + controller.error(new Error("process exited with code 1")); + }, + }); + const outputStream = new WritableStream(); + + const connection = new ClientSideConnection( + () => new MinimalTestClient(), + ndJsonStream(outputStream, inputStream), + ); + + await expect(connection.closed).resolves.toBeUndefined(); + expect(connection.signal.aborted).toBe(true); + }); + + it("rejects pending requests when input stream errors via ndJsonStream", async () => { + let errorController!: ReadableStreamDefaultController; + + const inputStream = new ReadableStream({ + start(controller) { + errorController = controller; + }, + }); + const outputStream = new WritableStream(); + + const connection = new ClientSideConnection( + () => new MinimalTestClient(), + ndJsonStream(outputStream, inputStream), + ); + + const requestPromise = connection.newSession({ + cwd: "/test", + mcpServers: [], + }); + + errorController.error(new Error("process exited with code 1")); + + await expect(requestPromise).rejects.toThrow( + "process exited with code 1", + ); + }); + it("rejects pending requests when the stream errors", async () => { let readableController!: ReadableStreamDefaultController; diff --git a/src/stream.ts b/src/stream.ts index d7f08c7..0f63602 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -63,10 +63,13 @@ export function ndJsonStream( } } } + } catch (err) { + controller.error(err); + return; } finally { reader.releaseLock(); - controller.close(); } + controller.close(); }, }); From f9a4eac38c6c23abbc1e872f02405213d5f2b6a2 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 8 Apr 2026 13:33:40 +0200 Subject: [PATCH 2/3] Fix lockfile --- package-lock.json | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6019adf..6266bf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,43 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", From 631927f4074e1ad47be24f1066493092a11cf914 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 8 Apr 2026 13:36:30 +0200 Subject: [PATCH 3/3] format --- src/acp.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/acp.test.ts b/src/acp.test.ts index c8ee09c..b1ac9e4 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -1053,9 +1053,7 @@ describe("Connection", () => { errorController.error(new Error("process exited with code 1")); - await expect(requestPromise).rejects.toThrow( - "process exited with code 1", - ); + await expect(requestPromise).rejects.toThrow("process exited with code 1"); }); it("rejects pending requests when the stream errors", async () => {