astral-sh/ruff · error · anyhow::Error

Received exit notification before a shutdown request

Error message

Received exit notification before a shutdown request

What it means

The LSP lifecycle mandates that the client send a `shutdown` request before the `exit` notification. ty's main loop enforces this: if `exit` arrives while `session.is_shutdown_requested()` is false, it returns this error, terminating the server with a failure exit code. This mirrors rust-analyzer's behavior and is intended to surface clients that violate the LSP shutdown handshake.

Solutions

  1. Send a `shutdown` request and await its response before sending `exit` (standard LSP lifecycle)
  2. If the client died unexpectedly, restart the language server cleanly - the error is expected to kill the process
  3. In test harnesses/scripts, add the shutdown step before exit
  4. Report the client integration if it systematically skips shutdown

Example fix

// before
send(lsp_server::Notification::new("exit".into(), null));
// after
let id = send(lsp_server::Request::new(id_counter.next(), "shutdown".into(), ()));
await_response(id);
send(lsp_server::Notification::new("exit".into(), null));
Defensive patterns

Strategy: fallback

Validate before calling

// Track the shutdown handshake before exiting
let shutdownAcked = false;
await sendRequest('shutdown', null).then(() => { shutdownAcked = true; });
if (!shutdownAcked) { log('shutdown request failed; still sending exit per fallback'); }
sendNotification('exit');

Try / catch

// Fallback wrapper for the exit path
async function shutdownAndExit() {
  try {
    await sendRequest('shutdown', null);
  } catch (err) {
    console.warn('shutdown request failed:', err);
  } finally {
    sendNotification('exit');
  }
}

Prevention

When it happens

Trigger: The client sends an `exit` notification (or closes the transport) without previously sending a successful `shutdown` request - e.g. killing the editor abruptly, a transport failure that drops the shutdown request, or a client implementation skipping the handshake.

Common situations: Users force-quitting an editor or crashing the LSP client; client integrations that terminate the server process directly; automated test harnesses that send `exit` immediately after startup; a lost/failed shutdown request over a flaky pipe or socket.

Related errors


AI-assisted analysis of astral-sh/ruff@5bb508f594 (2026-09-05). Data as JSON: /api/errors/0da3b6a6b569c6dc. Report an issue: GitHub.

Appendix: source

Thrown at crates/ty_server/src/server/main_loop.rs:68

                                    &req.method
                                );
                                client.respond_err(
                                    req.id,
                                    lsp_server::ResponseError {
                                        code: lsp_server::ErrorCode::InvalidRequest as i32,
                                        message: "Shutdown already requested".to_owned(),
                                        data: None,
                                    },
                                );
                                continue;
                            }

                            api::request(req)
                        }
                        Message::Notification(notification) => {
                            if notification.method == lsp_types::ExitNotification::METHOD.as_str() {
                                if !self.session.is_shutdown_requested() {
                                    return Err(anyhow!(
                                        "Received exit notification before a shutdown request"
                                    ));
                                }

                                tracing::debug!("Received exit notification, exiting");
                                return Ok(());
                            }

                            api::notification(notification)
                        }

                        // Handle the response from the client to a server request
                        Message::Response(response) => {
                            if let Some(handler) = self
                                .session
                                .request_queue_mut()
                                .outgoing_mut()
                                .complete(&response.id)

View on GitHub (pinned to 5bb508f594)