BoundaryML/baml · warning · anyhow
Server received an exit notification before a shutdown…
Error message
Server received an exit notification before a shutdown request was sent. Exiting...
What it means
Per the LSP specification, a client must send a 'shutdown' request before the 'exit' notification. If handle_shutdown sees an exit notification without a preceding shutdown request, it bails with this error to terminate the main loop, flagging a protocol violation by the client.
Solutions
- Ensure the client sends request 'shutdown' followed by notification 'exit' per LSP spec.
- If abrupt exit is acceptable, treat exit-without-shutdown as a normal termination instead of an error in the server.
- Check the client extension for code paths that skip shutdown (crash handlers, force-kill timers).
- Log at info/warn level and exit cleanly rather than propagating an anyhow bail.
Example fix
// before
anyhow::bail!("Server received an exit notification before a shutdown request was sent. Exiting...");
// after
log::warn!("Client sent exit without shutdown; exiting.");
return Ok(true); // stop the event loop without error Defensive patterns
Strategy: try-catch
Validate before calling
// ensure notification payload matches the expected type before sending JSON.stringify(notification.params); // catch non-serializable payloads early
Type guard
function hasValidParams(n: { params?: unknown }): boolean {
return n.params !== undefined;
} Try / catch
match notification.extract::<N>(method) {
Ok(n) => handle(n),
Err(ExtractError::JsonError(e)) => log::error!("bad notification params: {e}"),
Err(_) => unreachable!(),
} Prevention
- Keep notification param structs Option-typed where fields are optional.
- Version-check client and server so schemas stay aligned.
- Fuzz/test notifications with representative client payloads.
- Log the {json_err} detail for quick field-level debugging.
When it happens
Trigger: The client sends notification 'exit' without first sending the 'shutdown' request — e.g. the editor process is killed, the extension crashes mid-shutdown, or a custom/test client omits shutdown.
Common situations: Editors force-killing the server on window close, flaky client extensions shutting down abruptly, integration tests that only send exit, or race conditions in client teardown logic.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12).
Data as JSON: /api/errors/39770d82e774a282.
Report an issue: GitHub.
Appendix: source
Thrown at engine/language_server/src/server/connection.rs:139
);
self.sender.send(lsp::Message::Response(lsp::Response::new_err(
id.clone(),
lsp::ErrorCode::InvalidRequest as i32,
"Server received unexpected request while waiting for exit notification".to_string(),
)))?;
}
message => {
tracing::warn!(
"Server received unexpected message while waiting for exit notification: {message:?}"
);
}
}
}
}
lsp::Message::Notification(lsp::Notification { method, .. })
if method == lsp_types::notification::Exit::METHOD =>
{
anyhow::bail!("Server received an exit notification before a shutdown request was sent. Exiting...");
}
_ => Ok(false),
}
}
/// Join the I/O threads that underpin this connection.
/// This is guaranteed to be nearly immediate since
/// we close the only active channels to these threads prior
/// to joining them.
pub fn close(self) -> anyhow::Result<()> {
std::mem::drop(
Arc::into_inner(self.sender)
.expect("the client sender shouldn't have more than one strong reference"),
);
std::mem::drop(self.receiver);
self.threads.into_iter().try_for_each(|t| t.join())?;
Ok(())
}View on GitHub (pinned to bd85ce9dee)