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