Skip to main content

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}