lattice_lsp/error.rs
1//! Error surface for the LSP client. Kept narrow and shape-rich
2//! so the editor can branch on the failure (UI surface) instead
3//! of pattern-matching error strings.
4//!
5//! Mirrors the `RuntimeError` shape from `lattice-runtime`:
6//! protocol-level errors (`Busy`, `ActorGone`, `Cancelled`) are
7//! distinct from server-reported errors (`ResponseError` from
8//! the wire), which are distinct from transport / spawn failures
9//! (`Transport`, `Codec`).
10
11use thiserror::Error;
12
13use crate::codec::CodecError;
14use crate::framing::FrameError;
15use crate::jsonrpc::ResponseError;
16use crate::transport::TransportError;
17
18/// All failure modes a `ServerHandle` caller can observe.
19#[derive(Debug, Error)]
20pub enum LspError {
21 /// Failed to spawn the server binary or capture its stdio.
22 /// Usually the binary isn't on PATH or isn't executable.
23 #[error("transport: {0}")]
24 Transport(#[from] TransportError),
25
26 /// Codec-level failure -- bad framing, mid-message EOF,
27 /// invalid JSON-RPC body. These tear the transport down;
28 /// the supervisor may restart.
29 #[error("codec: {0}")]
30 Codec(#[from] CodecError),
31
32 /// Frame-header level failure. Distinct from `Codec` so the
33 /// supervisor can decide framing errors are unrecoverable
34 /// (server is sending garbage) while body errors might be a
35 /// version mismatch (server speaks an older spec).
36 #[error("framing: {0}")]
37 Framing(#[from] FrameError),
38
39 /// The server returned a JSON-RPC error response. Carries
40 /// the structured `ResponseError` so callers can branch on
41 /// `error_codes::REQUEST_CANCELLED` etc.
42 #[error("server error {}: {}", .0.code, .0.message)]
43 Server(ResponseError),
44
45 /// Outbound request failed because the actor's mailbox is
46 /// closed (the actor task has shut down).
47 #[error("server actor is no longer running")]
48 ActorGone,
49
50 /// The actor task accepted the request but the response
51 /// `oneshot::Sender` was dropped without sending. Means the
52 /// actor task panicked or shut down between accepting the
53 /// request and dispatching the response.
54 #[error("server actor dropped the response without sending")]
55 ResponseDropped,
56
57 /// The request was cancelled (either by the client via
58 /// `$/cancelRequest`, by content-modification supersession,
59 /// or by an explicit shutdown).
60 #[error("request cancelled")]
61 Cancelled,
62
63 /// Server failed the initialize handshake. Distinct from
64 /// `Server` so the actor knows the server is unusable and
65 /// not to send further requests.
66 #[error("initialize handshake failed: {0}")]
67 HandshakeFailed(String),
68
69 /// Server response could not be deserialized into the
70 /// requested type. The caller asked for `T`, the server sent
71 /// JSON that doesn't match `T`'s shape. Indicates either a
72 /// spec mismatch or a buggy server.
73 #[error("response deserialization failed: {0}")]
74 ResponseDecode(#[source] serde_json::Error),
75
76 /// Server expected the client to be initialized but got a
77 /// request before initialize completed. Should be
78 /// unreachable from outside the actor, but the path is here
79 /// in case a misbehaving call sneaks past gating.
80 #[error("server is not yet initialized")]
81 NotInitialized,
82}
83
84impl LspError {
85 /// True iff the failure is recoverable by retrying the
86 /// request. `Cancelled` and `Busy`-equivalent (mailbox closed)
87 /// are NOT retryable; transport errors are not retryable until
88 /// the supervisor brings the server back up.
89 pub fn is_retryable(&self) -> bool {
90 matches!(self, LspError::Cancelled)
91 }
92
93 /// True iff the failure means the server is dead from this
94 /// client's perspective. The supervisor should respawn.
95 pub fn is_fatal(&self) -> bool {
96 matches!(
97 self,
98 LspError::ActorGone
99 | LspError::Transport(_)
100 | LspError::Codec(_)
101 | LspError::Framing(_)
102 | LspError::HandshakeFailed(_)
103 )
104 }
105}
106
107/// Convenience alias so the rest of the crate doesn't have to
108/// re-spell `Result<T, LspError>` everywhere.
109pub type LspResult<T> = Result<T, LspError>;
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn classifies_fatal_vs_retryable() {
117 let fatal = LspError::ActorGone;
118 assert!(fatal.is_fatal());
119 assert!(!fatal.is_retryable());
120
121 let cancelled = LspError::Cancelled;
122 assert!(!cancelled.is_fatal());
123 assert!(cancelled.is_retryable());
124 }
125
126 #[test]
127 fn server_error_carries_structured_payload() {
128 let e = LspError::Server(ResponseError {
129 code: -32601,
130 message: "method not found".into(),
131 data: None,
132 });
133 assert!(!e.is_fatal());
134 assert_eq!(e.to_string(), "server error -32601: method not found");
135 }
136}