From b566bfdea0ff4df0050f4a05b8f5541deef0c96d Mon Sep 17 00:00:00 2001 From: Marcus Lagergren <1062473+lagergren@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:40:00 +0200 Subject: [PATCH] Handle the stop that follows a failed initialization When initialization fails, initialize() called `void this.stop()` while the client was still starting. The base shutdown rejects a client that is not running, so that promise rejected with "Client is not running and can't be stopped" and nothing handled it. Handle the rejection, logging it without a notification, as handleConnectionError does for the stop it starts. The client still calls stop(), because the Node client's shutdown override terminates the server process. A new test starts a client against a server that refuses initialization but keeps running, and checks that start() rejects with the server's error, that stop() is still called and the server process is ended, and that no rejection is left unhandled. --- client-node-tests/src/integration.test.ts | 43 +++++++++++++++++++ .../src/servers/failInitializeServer.ts | 13 ++++++ client/src/common/client.ts | 16 +++++-- 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 client-node-tests/src/servers/failInitializeServer.ts diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index 88459af0d..ae1ace00c 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -2307,6 +2307,49 @@ suite('Server tests', () => { } }).timeout(5000); + test('A refused initialization stops the client without an unhandled rejection', async () => { + const outputChannel = vscode.window.createOutputChannel('Initialization refused', { log: true }); + const client = new lsclient.LanguageClient('initialization-refused', 'Initialization refused', { + module: path.join(__dirname, './servers/failInitializeServer.js'), + transport: lsclient.TransportKind.ipc, + }, { + outputChannel, + initializationFailedHandler: () => false, + errorHandler: { + error: () => ({ action: lsclient.ErrorAction.Continue }), + closed: () => ({ action: lsclient.CloseAction.DoNotRestart, handled: true }), + } + }); + // The client moves from StartFailed to Stopped when the server's connection closes. + const stopped = new Promise(resolve => { + const listener = client.onDidChangeState(event => { + if (event.newState === lsclient.State.Stopped) { + listener.dispose(); + resolve(); + } + }); + }); + const unhandled: unknown[] = []; + const onUnhandled = (reason: unknown) => unhandled.push(reason); + process.on('unhandledRejection', onUnhandled); + const stop = sinon.spy(client, 'stop'); + try { + await assert.rejects(client.start(), /Initialization refused/); + // The client still stops after the failure, so the Node client terminates the + // server process, which closes the connection. The process has then also logged + // its exit, so the output channel can be disposed. + assert.strictEqual(stop.callCount, 1); + await stopped; + assert.deepStrictEqual(unhandled, []); + } finally { + process.off('unhandledRejection', onUnhandled); + stop.restore(); + await client.dispose(); + client.diagnostics?.dispose(); + outputChannel.dispose(); + } + }).timeout(10000); + test('Stop fails if server crashes after shutdown request', async () => { const serverOptions: lsclient.ServerOptions = { module: path.join(__dirname, './servers/crashOnShutdownServer.js'), diff --git a/client-node-tests/src/servers/failInitializeServer.ts b/client-node-tests/src/servers/failInitializeServer.ts new file mode 100644 index 000000000..35a244ca9 --- /dev/null +++ b/client-node-tests/src/servers/failInitializeServer.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createConnection, InitializeError, ResponseError } from 'vscode-languageserver/node'; + +const connection = createConnection(); +connection.onInitialize(() => { + // Refuse initialization but keep running, so the client still owns a live connection. + return new ResponseError(1, 'Initialization refused', { retry: false }); +}); +connection.listen(); diff --git a/client/src/common/client.ts b/client/src/common/client.ts index 449fb0c6a..d06f6a48a 100644 --- a/client/src/common/client.ts +++ b/client/src/common/client.ts @@ -1554,14 +1554,14 @@ export abstract class BaseLanguageClient implements FeatureClient { if (item && item.id === 'retry') { void this.initialize(connection); } else { - void this.stop(); + this.stopAfterFailedInitialize(); } }); } else { @@ -1569,12 +1569,22 @@ export abstract class BaseLanguageClient implements FeatureClient { + this.error(`Stopping server failed`, error, false); + }); + } + private _clientGetRootPath(): string | undefined { const folders = Workspace.workspaceFolders; if (!folders || folders.length === 0) {