Skip to main content

agent_client_protocol/jsonrpc/
transport_actor.rs

1use std::pin::pin;
2
3// Types re-exported from crate root
4use crate::RawJsonRpcResponse as Response;
5use crate::jsonrpc::{RawJsonRpcMessage, TransportFrame};
6use futures::StreamExt as _;
7use futures::channel::mpsc;
8
9fn parse_incoming_line(line: &str) -> TransportFrame {
10    let value = match serde_json::from_str::<serde_json::Value>(line) {
11        Ok(value) => value,
12        Err(error) => {
13            // Serde reasons can quote input, so log only the category and position.
14            tracing::debug!(
15                category = ?error.classify(),
16                line = error.line(),
17                column = error.column(),
18                "Failed to parse incoming JSON-RPC JSON"
19            );
20            return TransportFrame::Malformed {
21                raw: line.to_owned(),
22                error: crate::Error::parse_error().data(serde_json::json!({ "line": line })),
23            };
24        }
25    };
26
27    match value {
28        // JSON-RPC batching is not supported. An array, including an empty
29        // one, is one invalid request answered with a single error.
30        serde_json::Value::Array(_) => TransportFrame::Malformed {
31            raw: line.to_owned(),
32            error: crate::util::batch_not_supported(),
33        },
34        value => match serde_json::from_value(value) {
35            Ok(message) => TransportFrame::Single(message),
36            Err(error) => {
37                tracing::debug!(category = ?error.classify(), "Invalid JSON-RPC message");
38                TransportFrame::Malformed {
39                    raw: line.to_owned(),
40                    error: crate::Error::invalid_request(),
41                }
42            }
43        },
44    }
45}
46
47impl TransportFrame {
48    /// Parse one JSON-RPC wire value.
49    ///
50    /// Every malformed value is retained as an explicit malformed frame with
51    /// its original text. JSON-RPC batches are not supported: a JSON array is
52    /// a malformed frame with an invalid-request (`-32600`) error. Protocol
53    /// actors decide whether a malformed value is call-shaped and requires an
54    /// Error Response.
55    #[must_use]
56    pub fn parse_json(input: &str) -> Self {
57        parse_incoming_line(input)
58    }
59
60    /// Serialize this frame to its JSON-RPC wire representation.
61    ///
62    /// # Errors
63    ///
64    /// Returns an internal error if a valid message cannot be
65    /// serialized. Malformed frames return their original wire text unchanged.
66    pub fn to_json(&self) -> Result<String, crate::Error> {
67        match self {
68            Self::Single(message) => {
69                serde_json::to_string(message).map_err(crate::Error::into_internal_error)
70            }
71            Self::Malformed { raw, .. } => Ok(raw.clone()),
72        }
73    }
74}
75
76/// Transport outgoing actor for line streams: serializes [`TransportFrame`] values and yields
77/// lines.
78///
79/// This is a line-based variant of `transport_outgoing_actor` that works with a Sink<String>
80/// instead of an AsyncWrite byte stream. This enables interception of lines before they are
81/// written to the underlying transport.
82///
83/// This actor handles transport mechanics:
84/// - Serializes single messages and malformed values to JSON strings
85/// - Yields newline-terminated strings
86/// - Handles serialization errors
87///
88/// This is the transport layer - it has no knowledge of protocol semantics (IDs, correlation, etc.).
89async fn transport_outgoing_frames_actor(
90    transport_rx: impl futures::Stream<Item = TransportFrame>,
91    outgoing_lines: impl futures::Sink<String, Error = std::io::Error>,
92) -> Result<(), crate::Error> {
93    use futures::SinkExt;
94    let mut transport_rx = pin!(transport_rx);
95    let mut outgoing_lines = pin!(outgoing_lines);
96
97    while let Some(frame) = transport_rx.next().await {
98        let json_rpc_message = match frame {
99            TransportFrame::Single(message) => message,
100            TransportFrame::Malformed { raw, .. } => {
101                let raw = malformed_line_value(raw)?;
102                tracing::trace!(
103                    message = %crate::util::redacted_json_line(&raw),
104                    "Relaying invalid JSON-RPC value"
105                );
106                outgoing_lines
107                    .send(raw)
108                    .await
109                    .map_err(crate::Error::into_internal_error)?;
110                continue;
111            }
112        };
113        match serde_json::to_string(&json_rpc_message) {
114            Ok(line) => {
115                tracing::trace!(message = %crate::util::redacted_json_line(&line), "Sending JSON-RPC message");
116                outgoing_lines
117                    .send(line)
118                    .await
119                    .map_err(crate::Error::into_internal_error)?;
120            }
121
122            Err(serialization_error) => {
123                match json_rpc_message {
124                    RawJsonRpcMessage::Request(_) | RawJsonRpcMessage::Notification(_) => {
125                        // If we failed to serialize a request,
126                        // just ignore it.
127                        //
128                        // Q: (Maybe it'd be nice to "reply" with an error?)
129                        tracing::error!(
130                            ?serialization_error,
131                            "Failed to serialize request, ignoring"
132                        );
133                    }
134                    RawJsonRpcMessage::Response(response) => {
135                        // If we failed to serialize a *response*,
136                        // send an error in response.
137                        let id = match response {
138                            Response::Result { id, .. } | Response::Error { id, .. } => id,
139                        };
140                        tracing::error!(
141                            ?serialization_error,
142                            ?id,
143                            "Failed to serialize response, sending internal_error instead"
144                        );
145                        let error_line = serde_json::to_string(&RawJsonRpcMessage::response(
146                            id,
147                            Err(crate::Error::internal_error()),
148                        ))
149                        .unwrap();
150                        outgoing_lines
151                            .send(error_line)
152                            .await
153                            .map_err(crate::Error::into_internal_error)?;
154                    }
155                }
156            }
157        }
158    }
159    outgoing_lines
160        .close()
161        .await
162        .map_err(crate::Error::into_internal_error)
163}
164
165/// A newline writer whose sink close reaches the actual write half. An unfold
166/// sink can flush each line, but has no way to forward `AsyncWrite::close`.
167pub(super) struct LineWriter<W> {
168    writer: std::pin::Pin<Box<W>>,
169    bytes: Vec<u8>,
170    written: usize,
171    closing: bool,
172}
173
174impl<W> LineWriter<W> {
175    pub(super) fn new(writer: W) -> Self {
176        Self {
177            writer: Box::pin(writer),
178            bytes: Vec::new(),
179            written: 0,
180            closing: false,
181        }
182    }
183}
184
185impl<W: futures::AsyncWrite> futures::Sink<String> for LineWriter<W> {
186    type Error = std::io::Error;
187
188    fn poll_ready(
189        self: std::pin::Pin<&mut Self>,
190        cx: &mut std::task::Context<'_>,
191    ) -> std::task::Poll<Result<(), Self::Error>> {
192        self.poll_flush(cx)
193    }
194
195    fn start_send(self: std::pin::Pin<&mut Self>, line: String) -> Result<(), Self::Error> {
196        let this = self.get_mut();
197        this.bytes = line.into_bytes();
198        this.bytes.push(b'\n');
199        this.written = 0;
200        Ok(())
201    }
202
203    fn poll_flush(
204        self: std::pin::Pin<&mut Self>,
205        cx: &mut std::task::Context<'_>,
206    ) -> std::task::Poll<Result<(), Self::Error>> {
207        let this = self.get_mut();
208        while this.written < this.bytes.len() {
209            let count = futures::ready!(
210                this.writer
211                    .as_mut()
212                    .poll_write(cx, &this.bytes[this.written..])
213            )?;
214            if count == 0 {
215                return std::task::Poll::Ready(Err(std::io::ErrorKind::WriteZero.into()));
216            }
217            this.written += count;
218        }
219        this.bytes.clear();
220        this.written = 0;
221        this.writer.as_mut().poll_flush(cx)
222    }
223
224    fn poll_close(
225        mut self: std::pin::Pin<&mut Self>,
226        cx: &mut std::task::Context<'_>,
227    ) -> std::task::Poll<Result<(), Self::Error>> {
228        if !self.closing {
229            futures::ready!(self.as_mut().poll_flush(cx))?;
230            self.closing = true;
231        }
232        self.get_mut().writer.as_mut().poll_close(cx)
233    }
234}
235
236fn malformed_line_value(raw: String) -> Result<String, crate::Error> {
237    if !raw.contains('\r') && !raw.contains('\n') {
238        return Ok(raw);
239    }
240
241    match serde_json::from_str::<serde_json::Value>(&raw) {
242        Ok(value) => serde_json::to_string(&value),
243        Err(_) => serde_json::to_string(&raw),
244    }
245    .map_err(crate::Error::into_internal_error)
246}
247
248pub(super) async fn transport_outgoing_lines_actor(
249    transport_rx: impl futures::Stream<Item = TransportFrame>,
250    outgoing_lines: impl futures::Sink<String, Error = std::io::Error>,
251) -> Result<(), crate::Error> {
252    transport_outgoing_frames_actor(transport_rx, outgoing_lines).await
253}
254
255/// Transport incoming actor for line streams: parses lines into [`TransportFrame`] values.
256///
257/// This is a line-based variant of `transport_incoming_actor` that works with a
258/// Stream<Item = io::Result<String>> instead of an AsyncRead byte stream. This enables
259/// interception of lines before they are parsed.
260///
261/// This actor handles transport mechanics:
262/// - Reads lines from the stream
263/// - Parses individual messages
264/// - Retains malformed JSON, invalid messages, and unsupported batch arrays as
265///   malformed frames
266///
267/// This is the transport layer - it has no knowledge of protocol semantics.
268pub(super) async fn transport_incoming_lines_actor(
269    incoming_lines: impl futures::Stream<Item = std::io::Result<String>>,
270    transport_tx: mpsc::UnboundedSender<TransportFrame>,
271) -> Result<(), crate::Error> {
272    let mut incoming_lines = pin!(incoming_lines);
273    while let Some(line_result) = incoming_lines.next().await {
274        let line = line_result.map_err(crate::Error::into_internal_error)?;
275        tracing::trace!(message = %crate::util::redacted_json_line(&line), "Received JSON-RPC message");
276
277        transport_tx
278            .unbounded_send(parse_incoming_line(&line))
279            .map_err(crate::Error::into_internal_error)?;
280    }
281    Ok(())
282}
283
284#[cfg(test)]
285mod tests {
286    use std::sync::{Arc, Mutex};
287
288    use super::*;
289    use crate::ErrorCode;
290
291    #[derive(Default)]
292    struct PendingCloseWriter {
293        bytes: Vec<u8>,
294        close_polls: usize,
295    }
296
297    impl futures::AsyncWrite for PendingCloseWriter {
298        fn poll_write(
299            mut self: std::pin::Pin<&mut Self>,
300            _: &mut std::task::Context<'_>,
301            bytes: &[u8],
302        ) -> std::task::Poll<std::io::Result<usize>> {
303            self.bytes.extend_from_slice(bytes);
304            std::task::Poll::Ready(Ok(bytes.len()))
305        }
306
307        fn poll_flush(
308            self: std::pin::Pin<&mut Self>,
309            _: &mut std::task::Context<'_>,
310        ) -> std::task::Poll<std::io::Result<()>> {
311            assert_eq!(self.close_polls, 0, "do not flush again during shutdown");
312            std::task::Poll::Ready(Ok(()))
313        }
314
315        fn poll_close(
316            mut self: std::pin::Pin<&mut Self>,
317            cx: &mut std::task::Context<'_>,
318        ) -> std::task::Poll<std::io::Result<()>> {
319            self.close_polls += 1;
320            if self.close_polls == 1 {
321                cx.waker().wake_by_ref();
322                std::task::Poll::Pending
323            } else {
324                std::task::Poll::Ready(Ok(()))
325            }
326        }
327    }
328
329    #[test]
330    fn byte_writer_flushes_then_continues_pending_shutdown_without_reflushing() {
331        use futures::SinkExt as _;
332        let mut sink = LineWriter::new(PendingCloseWriter::default());
333        futures::executor::block_on(async {
334            sink.send("line".to_string()).await.unwrap();
335            sink.close().await.unwrap();
336        });
337        assert_eq!(sink.writer.bytes, b"line\n");
338        assert_eq!(sink.writer.close_polls, 2);
339    }
340
341    #[test]
342    fn preserves_malformed_standalone_response() {
343        let TransportFrame::Malformed { error, .. } = parse_incoming_line(
344            r#"{"jsonrpc":"2.0","id":1,"result":null,"error":{"code":-32603,"message":"Internal error"}}"#,
345        ) else {
346            panic!("expected one retained invalid response");
347        };
348
349        assert_eq!(error.code, ErrorCode::INVALID_REQUEST);
350    }
351
352    #[test]
353    fn preserves_malformed_call_shaped_standalone_message() {
354        let TransportFrame::Malformed { error, .. } =
355            parse_incoming_line(r#"{"jsonrpc":"2.0","id":1,"method":"one","result":null}"#)
356        else {
357            panic!("expected one invalid-request error");
358        };
359
360        assert_eq!(error.code, ErrorCode::INVALID_REQUEST);
361    }
362
363    #[test]
364    fn parses_valid_standalone_response() {
365        assert!(matches!(
366            parse_incoming_line(r#"{"jsonrpc":"2.0","id":1,"result":{"ok":true}}"#),
367            TransportFrame::Single(RawJsonRpcMessage::Response(_))
368        ));
369    }
370
371    #[test]
372    fn batches_are_one_invalid_request() {
373        for line in [
374            "[]",
375            "[1, 2, 3]",
376            r#"[{"jsonrpc":"2.0","id":1,"method":"one","params":{}}]"#,
377            r#"[{"jsonrpc":"2.0","id":1,"result":null}]"#,
378        ] {
379            let TransportFrame::Malformed { raw, error } = parse_incoming_line(line) else {
380                panic!("expected one invalid-request error for {line}");
381            };
382            assert_eq!(raw, line);
383            assert_eq!(error.code, ErrorCode::INVALID_REQUEST);
384        }
385    }
386
387    #[test]
388    fn malformed_json_is_a_parse_error() {
389        let TransportFrame::Malformed { raw, error } = parse_incoming_line("[") else {
390            panic!("expected one parse error");
391        };
392
393        assert_eq!(raw, "[");
394        assert_eq!(error.code, ErrorCode::PARSE_ERROR);
395    }
396
397    #[test]
398    fn valid_json_with_an_invalid_envelope_is_an_invalid_request() {
399        let TransportFrame::Malformed { raw, error } = parse_incoming_line("17") else {
400            panic!("expected one invalid-request error");
401        };
402
403        assert_eq!(raw, "17");
404        assert_eq!(error.code, ErrorCode::INVALID_REQUEST);
405    }
406
407    #[tokio::test]
408    async fn multiline_malformed_frame_is_written_as_one_line_value() {
409        let raw = "not json\r\n{\"jsonrpc\":\"2.0\",\"method\":\"injected\"}".to_string();
410        let captured = Arc::new(Mutex::new(Vec::new()));
411        let outgoing = futures::sink::unfold(captured.clone(), |captured, line| async move {
412            captured.lock().unwrap().push(line);
413            Ok::<_, std::io::Error>(captured)
414        });
415
416        transport_outgoing_frames_actor(
417            futures::stream::iter([TransportFrame::Malformed {
418                raw: raw.clone(),
419                error: crate::Error::parse_error(),
420            }]),
421            outgoing,
422        )
423        .await
424        .unwrap();
425
426        let lines = captured.lock().unwrap();
427        assert_eq!(lines.len(), 1);
428        assert!(!lines[0].contains('\r') && !lines[0].contains('\n'));
429        assert_eq!(serde_json::from_str::<String>(&lines[0]).unwrap(), raw);
430    }
431
432    #[test]
433    fn multiline_invalid_json_rpc_value_is_compacted_without_changing_value() {
434        let raw = "{\n  \"jsonrpc\": \"2.0\",\n  \"method\": 1\n}".to_string();
435        let expected = serde_json::from_str::<serde_json::Value>(&raw).unwrap();
436        let line = malformed_line_value(raw).unwrap();
437
438        assert!(!line.contains('\r') && !line.contains('\n'));
439        assert_eq!(
440            serde_json::from_str::<serde_json::Value>(&line).unwrap(),
441            expected
442        );
443    }
444}