astral-sh/ruff · warning

client exited without proper shutdown sequence

Error message

client exited without proper shutdown sequence

What it means

The LSP server's event loop (`main_loop`) polls `next_event()`, which returns `Ok(None)` when the message stream ends. If the stream ends without the client sending a proper `exit` notification after `shutdown`, the loop treats this as a protocol violation and aborts with this anyhow bail instead of exiting cleanly. LSP requires clients to follow the `initialize` -> `shutdown` -> `exit` sequence.

Solutions

  1. Ensure your LSP client sends a `shutdown` request and then an `exit` notification before closing the connection
  2. If you control the client, handle editor/process termination by sending the LSP shutdown/exit sequence in a cleanup handler
  3. Treat this message in server logs as a symptom of abrupt client disconnect, not an internal Ruff bug; restart the server if needed

Example fix

// custom LSP client cleanup
// before
process::exit(0);
// after
client.shutdown().await?;
client.exit().await; // sends the LSP exit notification
process::exit(0);
Defensive patterns

Strategy: try-catch

Try / catch

match server_exit_result { Err(e) if e.to_string().contains("proper shutdown sequence") => log.warn("client disconnected without LSP shutdown/exit"), other => other? }

Prevention

When it happens

Trigger: The LSP client (editor process) terminates abruptly or closes the stdio/IPC stream without sending the `shutdown` request followed by the `exit` notification; `next_event()` returns None on stream EOF before an Exit event was processed.

Common situations: Editor crashes or is force-killed while the Ruff language server is running; a custom or scripted LSP client that stops reading/writing and drops the connection; debugging sessions where the transport is closed manually.

Related errors


AI-assisted analysis of astral-sh/ruff@26f38c119c (2026-09-05). Data as JSON: /api/errors/91c5ee77385c09e0. Report an issue: GitHub.

Appendix: source

Thrown at crates/ruff_server/src/server/main_loop.rs:29

    server::{api, schedule},
    session::Client,
};

pub(crate) type MainLoopSender = crossbeam::channel::Sender<Event>;
pub(crate) type MainLoopReceiver = crossbeam::channel::Receiver<Event>;

impl Server {
    pub(super) fn main_loop(&mut self) -> crate::Result<()> {
        self.initialize(&Client::new(
            self.main_loop_sender.clone(),
            self.connection.sender.clone(),
        ));

        let mut scheduler = schedule::Scheduler::new(self.worker_threads);

        while let Ok(next_event) = self.next_event() {
            let Some(next_event) = next_event else {
                anyhow::bail!("client exited without proper shutdown sequence");
            };

            match next_event {
                Event::Message(msg) => {
                    let client = Client::new(
                        self.main_loop_sender.clone(),
                        self.connection.sender.clone(),
                    );

                    let task = match msg {
                        Message::Request(req) => {
                            self.session
                                .request_queue_mut()
                                .incoming_mut()
                                .register(req.id.clone(), req.method.clone());

                            if self.session.is_shutdown_requested() {
                                tracing::warn!(

View on GitHub (pinned to 26f38c119c)