Skip to main content

codex_codes/
error.rs

1//! Error types for the codex-codes crate.
2//!
3//! All fallible operations return [`Result<T>`], which uses [`enum@Error`] as the
4//! error type. The variants cover JSON serialization, I/O, protocol-level
5//! issues, and JSON-RPC errors from the app-server.
6
7use serde_json::Value;
8use thiserror::Error;
9
10/// Error type for parsing failures that preserves the raw frame data.
11///
12/// Returned inside [`Error::Deserialization`] when a message from the
13/// app-server fails to deserialize. The structured fields let consumers
14/// render the offending frame in bug reports without grepping logs.
15///
16/// Two failure modes are represented:
17///
18/// 1. **Bare JSON failure** — the line wasn't valid JSON, or the JSON didn't
19///    match the [`JsonRpcMessage`](crate::JsonRpcMessage) envelope. `raw_line`
20///    is the original line from stdout. `raw_json` is populated when the line
21///    parsed as JSON but didn't fit the envelope; `None` when the line wasn't
22///    even JSON. `method` is `None`.
23///
24/// 2. **Typed decode failure** — the envelope parsed fine (so the JSON-RPC
25///    `method` is known), but the typed payload decode
26///    (`Notification::from_envelope` / `ServerRequest::from_envelope`) failed
27///    on the `params`. `method` carries the JSON-RPC method name. `raw_json`
28///    carries the `params` value. `raw_line` is the re-serialized envelope —
29///    wire-equivalent to what came in, suitable for pasting into a bug report.
30#[derive(Debug, Clone)]
31pub struct ParseError {
32    /// Line from stdout (or re-serialized envelope, for typed-decode failures).
33    pub raw_line: String,
34    /// Parsed JSON value when available (the `params` for typed-decode
35    /// failures; the parsed line for envelope-shape failures).
36    pub raw_json: Option<Value>,
37    /// The underlying serde error description.
38    pub error_message: String,
39    /// JSON-RPC `method` name when the failure happened at the typed-decode
40    /// stage. `None` for bare-JSON / envelope-shape failures.
41    pub method: Option<String>,
42}
43
44impl ParseError {
45    /// Build a [`ParseError`] for a bare-JSON or envelope-shape failure.
46    pub fn from_line(line: impl Into<String>, error: serde_json::Error) -> Self {
47        let raw_line = line.into();
48        let raw_json = serde_json::from_str::<Value>(&raw_line).ok();
49        ParseError {
50            raw_line,
51            raw_json,
52            error_message: error.to_string(),
53            method: None,
54        }
55    }
56
57    /// Build a [`ParseError`] for a typed-decode failure on a notification or
58    /// request whose envelope parsed but whose `params` did not match.
59    ///
60    /// `raw_line` is reconstructed by re-serializing the envelope so consumers
61    /// can render the full offending frame even though the original line was
62    /// already consumed by the envelope decode.
63    pub fn from_envelope(
64        method: impl Into<String>,
65        params: Option<Value>,
66        error: serde_json::Error,
67    ) -> Self {
68        let method = method.into();
69        let raw_line = match &params {
70            Some(p) => format!(
71                r#"{{"method":{},"params":{}}}"#,
72                serde_json::to_string(&method).unwrap_or_else(|_| "\"<unserializable>\"".into()),
73                serde_json::to_string(p).unwrap_or_else(|_| "null".into()),
74            ),
75            None => format!(
76                r#"{{"method":{}}}"#,
77                serde_json::to_string(&method).unwrap_or_else(|_| "\"<unserializable>\"".into()),
78            ),
79        };
80        ParseError {
81            raw_line,
82            raw_json: params,
83            error_message: error.to_string(),
84            method: Some(method),
85        }
86    }
87}
88
89impl std::fmt::Display for ParseError {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match &self.method {
92            Some(m) => write!(
93                f,
94                "Failed to decode params for method {:?}: {} (raw: {})",
95                m, self.error_message, self.raw_line
96            ),
97            None => write!(
98                f,
99                "Failed to parse JSON-RPC message: {} (raw: {})",
100                self.error_message, self.raw_line
101            ),
102        }
103    }
104}
105
106impl std::error::Error for ParseError {}
107
108/// All possible errors from codex-codes operations.
109#[derive(Error, Debug)]
110pub enum Error {
111    /// JSON serialization or deserialization failed.
112    ///
113    /// Returned when request parameters can't be serialized or
114    /// response payloads don't match expected types.
115    #[error("JSON error: {0}")]
116    Json(#[from] serde_json::Error),
117
118    /// An I/O error occurred communicating with the app-server process.
119    ///
120    /// Common causes: process not found, pipe broken, permission denied.
121    #[error("IO error: {0}")]
122    Io(#[from] std::io::Error),
123
124    /// A protocol-level error (e.g., missing stdin/stdout pipes).
125    #[error("Protocol error: {0}")]
126    Protocol(String),
127
128    /// The app-server connection was closed unexpectedly.
129    #[error("Connection closed")]
130    ConnectionClosed,
131
132    /// A message from the server could not be deserialized.
133    ///
134    /// Carries a [`ParseError`] with the offending `method` (when known),
135    /// raw frame, and the underlying serde diagnostic. If you encounter this,
136    /// please report it with the `raw_line` — it likely indicates a protocol
137    /// change.
138    #[error("Deserialization error: {0}")]
139    Deserialization(#[from] ParseError),
140
141    /// The app-server process exited with a non-zero status.
142    #[error("Process exited with status {0}: {1}")]
143    ProcessFailed(i32, String),
144
145    /// The server returned a JSON-RPC error response.
146    ///
147    /// Contains the error code and message from the server.
148    /// See the Codex CLI docs for error code meanings.
149    #[error("JSON-RPC error ({code}): {message}")]
150    JsonRpc { code: i64, message: String },
151
152    /// The server closed the connection (EOF on stdout).
153    ///
154    /// Returned by `request()` if the server exits mid-conversation.
155    #[error("Server closed connection")]
156    ServerClosed,
157
158    /// The CLI binary could not be found on PATH.
159    #[error("Binary not found: '{name}' is not on PATH. Is it installed?")]
160    BinaryNotFound { name: String },
161
162    /// An unclassified error.
163    #[error("Unknown error: {0}")]
164    Unknown(String),
165}
166
167/// A `Result` type alias using [`enum@Error`].
168pub type Result<T> = std::result::Result<T, Error>;
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use serde_json::json;
174
175    fn serde_err(s: &str) -> serde_json::Error {
176        serde_json::from_str::<Value>(s).unwrap_err()
177    }
178
179    #[test]
180    fn parse_error_from_line_valid_json_populates_raw_json() {
181        let line = r#"{"foo":"bar"}"#;
182        // Force a downstream typed-decode failure to produce a serde_json::Error;
183        // we just need *some* error to attach.
184        let err = serde_json::from_str::<i32>(line).unwrap_err();
185
186        let pe = ParseError::from_line(line, err);
187        assert_eq!(pe.raw_line, line);
188        assert_eq!(pe.raw_json, Some(json!({"foo": "bar"})));
189        assert!(pe.method.is_none());
190        assert!(!pe.error_message.is_empty());
191    }
192
193    #[test]
194    fn parse_error_from_line_invalid_json_has_none_raw_json() {
195        let line = "not-json{";
196        let err = serde_err(line);
197
198        let pe = ParseError::from_line(line, err);
199        assert_eq!(pe.raw_line, line);
200        assert!(pe.raw_json.is_none());
201        assert!(pe.method.is_none());
202    }
203
204    #[test]
205    fn parse_error_from_envelope_carries_method_params_and_reconstructs_line() {
206        let params = json!({"callId": null, "kind": "fileChange"});
207        let err = serde_json::from_value::<i32>(params.clone()).unwrap_err();
208
209        let pe =
210            ParseError::from_envelope("item/fileChange/requestApproval", Some(params.clone()), err);
211
212        assert_eq!(
213            pe.method.as_deref(),
214            Some("item/fileChange/requestApproval")
215        );
216        assert_eq!(pe.raw_json, Some(params.clone()));
217
218        // raw_line round-trips to the same shape (re-parse what we built).
219        let v: Value = serde_json::from_str(&pe.raw_line).unwrap();
220        assert_eq!(v["method"], "item/fileChange/requestApproval");
221        assert_eq!(v["params"], params);
222    }
223
224    #[test]
225    fn parse_error_from_envelope_handles_missing_params() {
226        let err = serde_err("not json");
227        let pe = ParseError::from_envelope("turn/completed", None, err);
228        let v: Value = serde_json::from_str(&pe.raw_line).unwrap();
229        assert_eq!(v["method"], "turn/completed");
230        assert!(v.get("params").is_none());
231        assert!(pe.raw_json.is_none());
232    }
233
234    #[test]
235    fn error_deserialization_display_includes_method_and_raw() {
236        let params = json!({"foo": 1});
237        let err = serde_err("not json");
238        let pe = ParseError::from_envelope("item/bogus", Some(params), err);
239        let e: Error = Error::Deserialization(pe);
240        let rendered = format!("{}", e);
241        assert!(rendered.contains("item/bogus"), "got: {}", rendered);
242        assert!(rendered.contains("foo"), "got: {}", rendered);
243    }
244}