Skip to content

Commit 6551152

Browse files
Suppress JSON-RPC writer rejections during client teardown
forceStop()/stop() dispose the JSON-RPC connection and destroy the underlying socket. If vscode-jsonrpc still has an in-flight write at that moment — most commonly the auto-generated response to a server->client request (a tool, hook, userInput, or LLM-inference handler that resolved just before teardown) — the write rejects with ERR_STREAM_DESTROYED. That response write is internal to vscode-jsonrpc and awaited by nobody, so the rejection surfaces as an unhandled promise rejection (observed intermittently in the e2e suite, originating from pending_work_resume's cold-resume forceStop path, but reproducible by any consumer that forceStop()s while a server->client request is in flight). Wrap the StreamMessageWriter so write failures can be swallowed, but only once a teardown-in-progress flag is set immediately before connection.dispose(). The writer still fires its error event (forwarded to MessageConnection.onError) and dispose() still rejects pending requests, so no signal is lost. Outside teardown the flag stays false, so write failures propagate normally and in-flight requests continue to fail fast. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 549b83b commit 6551152

1 file changed

Lines changed: 51 additions & 5 deletions

File tree

nodejs/src/client.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { fileURLToPath } from "node:url";
2121
import {
2222
createMessageConnection,
2323
ErrorCodes,
24+
type Message,
2425
MessageConnection,
2526
ResponseError,
2627
StreamMessageReader,
@@ -375,10 +376,40 @@ function getBundledCliPath(): string {
375376
* await client.stop();
376377
* ```
377378
*/
379+
/**
380+
* A {@link StreamMessageWriter} that suppresses write failures while the client
381+
* is tearing down its transport.
382+
*
383+
* During `stop()`/`forceStop()` the runtime's end of the pipe can close while
384+
* vscode-jsonrpc still has an in-flight write — most commonly the
385+
* auto-generated response to a server→client request (tool/hook/userInput/LLM
386+
* inference handler) that resolved just before teardown. That write rejects
387+
* with `ERR_STREAM_DESTROYED`, and because the response write is internal to
388+
* vscode-jsonrpc and awaited by nobody, the rejection surfaces as an unhandled
389+
* rejection. The writer still fires its `error` event (forwarded to
390+
* {@link MessageConnection.onError}), so swallowing the rejected promise during
391+
* teardown loses no signal. Outside teardown the flag stays `false`, so write
392+
* failures propagate normally and in-flight requests still fail fast.
393+
*/
394+
class TeardownResilientStreamMessageWriter extends StreamMessageWriter {
395+
public suppressWriteErrors = false;
396+
397+
public override async write(msg: Message): Promise<void> {
398+
try {
399+
await super.write(msg);
400+
} catch (error) {
401+
if (!this.suppressWriteErrors) {
402+
throw error;
403+
}
404+
}
405+
}
406+
}
407+
378408
export class CopilotClient {
379409
private cliStartTimeout: ReturnType<typeof setTimeout> | null = null;
380410
private cliProcess: ChildProcess | null = null;
381411
private connection: MessageConnection | null = null;
412+
private messageWriter: TeardownResilientStreamMessageWriter | null = null;
382413
private socket: Socket | null = null;
383414
private runtimePort: number | null = null;
384415
private actualHost: string = "localhost";
@@ -817,7 +848,13 @@ export class CopilotClient {
817848
}
818849
}
819850

820-
// Close connection
851+
// Close connection. Suppress writer failures first: tearing down the
852+
// transport can reject an in-flight server→client response write with
853+
// ERR_STREAM_DESTROYED, which would otherwise surface as an unhandled
854+
// rejection. dispose() still rejects any pending requests.
855+
if (this.messageWriter) {
856+
this.messageWriter.suppressWriteErrors = true;
857+
}
821858
if (this.connection) {
822859
try {
823860
this.connection.dispose();
@@ -829,6 +866,7 @@ export class CopilotClient {
829866
);
830867
}
831868
this.connection = null;
869+
this.messageWriter = null;
832870
this._rpc = null;
833871
this._internalRpc = null;
834872
}
@@ -944,14 +982,19 @@ export class CopilotClient {
944982
// Clear sessions immediately without trying to destroy them
945983
this.sessions.clear();
946984

947-
// Force close connection
985+
// Force close connection. Suppress writer failures first so teardown
986+
// write rejections don't surface as unhandled rejections.
987+
if (this.messageWriter) {
988+
this.messageWriter.suppressWriteErrors = true;
989+
}
948990
if (this.connection) {
949991
try {
950992
this.connection.dispose();
951993
} catch {
952994
// Ignore errors during force stop
953995
}
954996
this.connection = null;
997+
this.messageWriter = null;
955998
this._rpc = null;
956999
this._internalRpc = null;
9571000
}
@@ -2260,9 +2303,10 @@ export class CopilotClient {
22602303
});
22612304

22622305
// Create JSON-RPC connection over stdin/stdout
2306+
this.messageWriter = new TeardownResilientStreamMessageWriter(this.cliProcess.stdin!);
22632307
this.connection = createMessageConnection(
22642308
new StreamMessageReader(this.cliProcess.stdout!),
2265-
new StreamMessageWriter(this.cliProcess.stdin!)
2309+
this.messageWriter
22662310
);
22672311

22682312
this.attachConnectionHandlers();
@@ -2278,9 +2322,10 @@ export class CopilotClient {
22782322
}
22792323

22802324
// Create JSON-RPC connection over stdin/stdout
2325+
this.messageWriter = new TeardownResilientStreamMessageWriter(process.stdout);
22812326
this.connection = createMessageConnection(
22822327
new StreamMessageReader(process.stdin),
2283-
new StreamMessageWriter(process.stdout)
2328+
this.messageWriter
22842329
);
22852330

22862331
this.attachConnectionHandlers();
@@ -2306,9 +2351,10 @@ export class CopilotClient {
23062351
this.socket.connect(this.runtimePort!, this.actualHost, () => {
23072352
clearTimeout(connectionTimeout);
23082353
// Create JSON-RPC connection
2354+
this.messageWriter = new TeardownResilientStreamMessageWriter(this.socket!);
23092355
this.connection = createMessageConnection(
23102356
new StreamMessageReader(this.socket!),
2311-
new StreamMessageWriter(this.socket!)
2357+
this.messageWriter
23122358
);
23132359

23142360
this.attachConnectionHandlers();

0 commit comments

Comments
 (0)