Skip to main content

agent_client_protocol/jsonrpc/
raw_error.rs

1//! Transport-level error objects, before choosing an application protocol.
2
3use agent_client_protocol_schema::MaybeUndefined;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6
7use super::RawJsonRpcId;
8
9/// A JSON-RPC error without ACP-specific interpretation.
10///
11/// Raw transports and relays preserve unknown fields and distinguish omitted
12/// `data` from explicit JSON null. Convert to [`crate::Error`] only when
13/// dispatching an ACP response; other protocols have their own error domains.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[non_exhaustive]
16pub struct RawJsonRpcError {
17    /// The peer's numeric error code, not an ACP [`crate::ErrorCode`].
18    pub code: i32,
19    /// The peer's error message.
20    pub message: String,
21    /// Optional error data. Explicit null is retained separately from omission.
22    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
23    pub data: MaybeUndefined<Value>,
24    /// Additional fields on the error object.
25    #[serde(flatten)]
26    pub extra: Map<String, Value>,
27}
28
29/// A transport-level JSON-RPC response with an opaque result or raw error.
30///
31/// Errors are boxed so their extensible representation does not enlarge every
32/// request, notification, and queued frame.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(untagged)]
35#[allow(
36    clippy::exhaustive_enums,
37    reason = "JSON-RPC defines exactly these response kinds"
38)]
39pub enum RawJsonRpcResponse {
40    /// A successful response.
41    Result {
42        /// The ID of the request this response answers.
43        id: RawJsonRpcId,
44        /// The result, uninterpreted.
45        // Require the key even though `Value` accepts null.
46        #[serde(deserialize_with = "Deserialize::deserialize")]
47        result: Value,
48    },
49    /// An Error Response.
50    Error {
51        /// The ID of the request this response answers, or `null` when it
52        /// could not be determined.
53        id: RawJsonRpcId,
54        /// The error, uninterpreted.
55        error: Box<RawJsonRpcError>,
56    },
57}
58
59impl RawJsonRpcResponse {
60    /// Builds a response from a Rust [`Result`].
61    #[must_use]
62    pub fn new(id: impl Into<RawJsonRpcId>, result: Result<Value, Box<RawJsonRpcError>>) -> Self {
63        let id = id.into();
64        match result {
65            Ok(result) => Self::Result { id, result },
66            Err(error) => Self::Error { id, error },
67        }
68    }
69
70    /// The ID of the request this response answers.
71    #[must_use]
72    pub fn id(&self) -> &RawJsonRpcId {
73        match self {
74            Self::Result { id, .. } | Self::Error { id, .. } => id,
75        }
76    }
77}
78
79impl RawJsonRpcError {
80    /// Construct an error without data or extension fields.
81    #[must_use]
82    pub fn new(code: i32, message: impl Into<String>) -> Self {
83        Self {
84            code,
85            message: message.into(),
86            data: MaybeUndefined::Undefined,
87            extra: Map::new(),
88        }
89    }
90
91    /// Set error data, preserving explicit null.
92    #[must_use]
93    pub fn data(mut self, data: Value) -> Self {
94        self.data = if data.is_null() {
95            MaybeUndefined::Null
96        } else {
97            MaybeUndefined::Value(data)
98        };
99        self
100    }
101
102    /// Interpret this error as an ACP response for the typed dispatcher.
103    ///
104    /// ACP's error type does not model extension fields, so this intentionally
105    /// discards `extra`. Do not use it when forwarding raw frames or interpreting
106    /// errors from another protocol.
107    #[must_use]
108    pub fn into_acp_error(self) -> crate::Error {
109        let mut error = crate::Error::new(self.code, self.message);
110        error.data = match self.data {
111            MaybeUndefined::Undefined => None,
112            MaybeUndefined::Null => Some(Value::Null),
113            MaybeUndefined::Value(data) => Some(data),
114        };
115        error
116    }
117}
118
119impl From<crate::Error> for RawJsonRpcError {
120    fn from(error: crate::Error) -> Self {
121        let raw = Self::new(error.code.into(), error.message);
122        match error.data {
123            Some(data) => raw.data(data),
124            None => raw,
125        }
126    }
127}
128
129impl std::fmt::Display for RawJsonRpcError {
130    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(formatter, "{} ({})", self.message, self.code)
132    }
133}
134
135impl std::error::Error for RawJsonRpcError {}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::{Channel, RawJsonRpcMessage, TransportFrame};
141    use futures::StreamExt as _;
142    use serde_json::json;
143
144    #[test]
145    fn raw_errors_preserve_codes_data_presence_and_extensions() {
146        for code in [i32::MIN, -32000, -32602, 0, 12345, i32::MAX] {
147            for data in [None, Some(Value::Null), Some(json!({"detail":[1,2]}))] {
148                let mut error = json!({
149                    "code": code,
150                    "message": "peer",
151                    "extension": {"retry": true},
152                    "_meta": {"opaque": "kept"}
153                });
154                if let Some(data) = &data {
155                    error["data"] = data.clone();
156                }
157                let wire = json!({"jsonrpc":"2.0", "id":"logical", "error":error});
158                let parsed: RawJsonRpcMessage = serde_json::from_value(wire.clone()).unwrap();
159                assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
160                let RawJsonRpcMessage::Response(RawJsonRpcResponse::Error { error, .. }) = parsed
161                else {
162                    panic!("expected a raw error response");
163                };
164                assert_eq!(error.code, code);
165                match data {
166                    None => assert!(error.data.is_undefined()),
167                    Some(Value::Null) => assert!(error.data.is_null()),
168                    Some(value) => assert_eq!(error.data.value(), Some(&value)),
169                }
170            }
171        }
172    }
173
174    #[tokio::test]
175    async fn raw_responses_survive_framing_and_channel_relay() {
176        let error = json!({"code":-32000, "message":"peer", "extension":{"retry":true}});
177        for wire in [
178            json!({"jsonrpc":"2.0", "id":"error", "error":error}),
179            json!([
180                {"jsonrpc":"2.0", "id":"omitted", "error":error},
181                {"jsonrpc":"2.0", "id":"null", "error":{
182                    "code":12345, "message":"peer", "data":null, "_meta":{"opaque":"kept"}
183                }},
184                {"jsonrpc":"2.0", "id":"success", "result":null}
185            ]),
186        ] {
187            let frame = TransportFrame::parse_json(&wire.to_string());
188            let (source, relay_in) = Channel::duplex();
189            let (relay_out, mut destination) = Channel::duplex();
190            source.tx.unbounded_send(frame).unwrap();
191            drop(source);
192            Channel {
193                rx: relay_in.rx,
194                tx: relay_out.tx,
195            }
196            .copy()
197            .await
198            .unwrap();
199            let received = destination.rx.next().await.unwrap();
200            let received: Value = serde_json::from_str(&received.to_json().unwrap()).unwrap();
201            assert_eq!(received, wire);
202        }
203    }
204
205    #[test]
206    fn acp_error_interpretation_is_explicit_and_keeps_data_presence() {
207        for (code, expected_code) in [
208            (-32000, crate::ErrorCode::AUTH_REQUIRED),
209            (12345, crate::ErrorCode::new(12345)),
210        ] {
211            for data in [None, Some(Value::Null), Some(json!({"detail":"kept"}))] {
212                let mut raw = RawJsonRpcError::new(code, "peer");
213                raw.extra.insert("extension".into(), json!(true));
214                if let Some(data) = &data {
215                    raw = raw.data(data.clone());
216                }
217                let error = raw.into_acp_error();
218                assert_eq!(error.code, expected_code);
219                assert_eq!(error.message, "peer");
220                assert_eq!(error.data, data);
221                let roundtrip = RawJsonRpcError::from(error);
222                assert_eq!(roundtrip.code, code);
223                assert!(roundtrip.extra.is_empty());
224                match data {
225                    None => assert!(roundtrip.data.is_undefined()),
226                    Some(Value::Null) => assert!(roundtrip.data.is_null()),
227                    Some(value) => assert_eq!(roundtrip.data.value(), Some(&value)),
228                }
229            }
230        }
231    }
232
233    #[test]
234    fn acp_response_constructor_preserves_explicit_null_data() {
235        for data in [None, Some(Value::Null), Some(json!({"detail":"kept"}))] {
236            let mut error = crate::Error::invalid_params();
237            error.data = data.clone();
238            let message = RawJsonRpcMessage::response(RawJsonRpcId::Null, Err(error));
239            let wire = serde_json::to_value(message).unwrap();
240            assert_eq!(wire["error"]["code"], -32602);
241            assert_eq!(wire["error"].get("data"), data.as_ref());
242        }
243    }
244
245    #[test]
246    fn malformed_raw_errors_are_still_rejected() {
247        for error in [
248            Value::Null,
249            json!({"code":-32000}),
250            json!({"message":"peer"}),
251            json!({"code":null, "message":"peer"}),
252            json!({"code":"-32000", "message":"peer"}),
253            json!({"code":1.5, "message":"peer"}),
254            json!({"code":-32000, "message":null}),
255        ] {
256            assert!(
257                serde_json::from_value::<RawJsonRpcMessage>(
258                    json!({"jsonrpc":"2.0", "id":1, "error":error})
259                )
260                .is_err()
261            );
262        }
263    }
264}