Skip to main content

protocol_droid/domain/jsonrpc/
envelope.rs

1//! The borrowed JSON-RPC envelope — what a frame *is*, before anything
2//! decides what to do about it.
3//!
4//! # Borrowed, not owned, and the reason is the hot path
5//!
6//! [`Frame`] holds `&str` and `&RawValue` pointing into the frame buffer. A
7//! numeric id is an `i64` copied out of the buffer, so routing a frame costs no
8//! allocation at all — which is what "the envelope path must not allocate for a
9//! numeric id" means in practice. A string id borrows the buffer rather than
10//! building an `Arc<str>`; the owned [`RequestId`](crate::domain::request_id::RequestId) is built later, at the point
11//! where a value has to outlive the buffer (the in-flight map, a log line, a
12//! record/replay file).
13//!
14//! Splitting the two is not a micro-optimisation. The IR's [`RequestId`](crate::domain::request_id::RequestId) is
15//! deliberately non-`Copy` and allocates its `Arc<str>` at construction, which
16//! is right for a stored value and wrong for a value that is about to be
17//! matched against a method and dropped.
18//!
19//! # One pass, and the decision is stored
20//!
21//! JSON-RPC 2.0 is a four-way choice, and a message that is not exactly one of
22//! the four is `-32600`. `decode` resolves that choice once and writes the
23//! answer into [`MessageKind`]; [`Envelope::kind`] is therefore not a question
24//! the router re-asks. A frame that reached the router has already been
25//! validated as one of the four shapes, so no downstream code has to decide
26//! whether a `method` next to an `id` is legal.
27//!
28//! This is the project's "parse, don't validate" rule applied to the envelope:
29//! there is no `Envelope` in which the caller has to work out what kind of
30//! message it holds.
31
32use std::borrow::Cow;
33
34use serde::Deserialize;
35use serde_json::value::RawValue;
36
37/// The `jsonrpc` member every message must carry.
38const JSONRPC_VERSION: &str = "2.0";
39
40/// A JSON-RPC method name, borrowed from the frame unless it needed unescaping.
41///
42/// A `Cow` rather than a `&str` for the same reason [`WireId`] is one:
43/// `serde_json` refuses to lend a borrow for a string containing an escape, and
44/// a peer may spell a method with one. The common case — `tools/list` written
45/// plainly — allocates nothing.
46pub type Method<'a> = Cow<'a, str>;
47
48/// A JSON-RPC id as it appears on the wire, borrowed from the frame buffer.
49///
50/// A number is copied out — it is a `Copy` `i64`, so that arm never allocates.
51/// A string is a [`Cow`]: borrowed straight out of the buffer when it has no
52/// escape, and owned only when it does.
53///
54/// # Why a `Cow` and not a `&str`
55///
56/// `serde_json` will not hand back a borrowed `&str` for a string containing an
57/// escape — `"a\"b"` has to be unescaped, and unescaping means allocating, so
58/// the parse fails rather than lie about the borrow. An id containing an escape
59/// is perfectly legal JSON and a peer can send one, so a bare `&'a str` here
60/// would reject a request that must be answered.
61///
62/// The price is that `WireId` is no longer `Copy`, which propagates to
63/// `Envelope` and `Routed`. That is the right trade: a `Copy` derive on a type
64/// that cannot represent a valid id is not a saving, it is a bug waiting to
65/// compile. The common case — an unescaped uuid or a small integer — still
66/// allocates nothing.
67///
68/// # The lossy-looking `to_json`
69///
70/// [`to_json`](Self::to_json) re-escapes rather than splicing the original raw
71/// text, so a string id that was sent escaped comes back escaped in the same
72/// way. That is what makes the response's id parse to the value the peer sent.
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub enum WireId<'a> {
75    /// A numeric id.
76    Number(i64),
77    /// A string id, borrowed from the buffer unless it needed unescaping.
78    Text(Cow<'a, str>),
79}
80
81impl<'a> WireId<'a> {
82    /// This id, owned.
83    ///
84    /// A handler can outlive the frame it was invoked for — a tool that asks
85    /// its peer a question keeps running while the request that called it is
86    /// being answered — so an id borrowed from a request body has to become an
87    /// owned one before it crosses into anything longer-lived than that body.
88    #[must_use]
89    pub fn to_owned(&self) -> WireId<'static> {
90        match self {
91            Self::Number(value) => WireId::Number(*value),
92            Self::Text(text) => WireId::Text(Cow::Owned(text.as_ref().to_owned())),
93        }
94    }
95
96    /// Parses an id from its raw JSON, borrowed.
97    ///
98    /// The spec allows a number or a string and nothing else, so `null`,
99    /// `true`, an array, and a fractional number are all rejected. A
100    /// fractional number is rejected rather than truncated: `1.5` is not id
101    /// `1`, and quietly rounding it would make two different ids collide.
102    ///
103    /// # Errors
104    ///
105    /// [`WireIdError::NotAnId`] if the value is not a number or a string.
106    /// [`WireIdError::NotAnInteger`] if the number has a fractional part.
107    pub fn from_raw(value: &'a RawValue) -> Result<Self, WireIdError> {
108        let raw = value.get();
109        // A string first, borrowed: the text arm is zero-copy and this is the
110        // only arm that can be. A leading quote is the discriminator, and
111        // `RawValue` is already a syntactically complete JSON value, so the
112        // first byte decides.
113        if raw.starts_with('"') {
114            // A `&str` first: it succeeds and borrows for the overwhelmingly
115            // common unescaped id, with no allocation at all. It fails for an
116            // id containing an escape, because unescaping means allocating —
117            // and only then does a `String` get allocated to hold the result.
118            return Ok(match serde_json::from_str::<&'a str>(raw) {
119                Ok(borrowed) => Self::Text(Cow::Borrowed(borrowed)),
120                Err(_) => match serde_json::from_str::<String>(raw) {
121                    Ok(owned) => Self::Text(Cow::Owned(owned)),
122                    Err(_) => return Err(WireIdError::NotAnId),
123                },
124            });
125        }
126        // Then an integer, which is `Copy` and therefore free to extract. A
127        // fractional number parses as `f64` and is refused rather than
128        // truncated: `1.5` is not id `1`, and rounding it would make two
129        // different ids collide on the way onto the in-flight map.
130        match serde_json::from_str::<i64>(raw) {
131            Ok(number) => Ok(Self::Number(number)),
132            // A JSON number that is not an `i64` is either fractional or out
133            // of range. The specification says an id must not contain
134            // fractional parts; an integer too large for `i64` fails the same
135            // check for the same reason, and both are told apart by the text.
136            Err(_) if serde_json::from_str::<f64>(raw).is_ok() => Err(WireIdError::NotAnInteger),
137            Err(_) => Err(WireIdError::NotAnId),
138        }
139    }
140
141    /// A borrowed string id.
142    ///
143    /// Does not allocate. The type is a `Cow` only because an id containing a
144    /// JSON escape cannot be borrowed out of the buffer; a caller that already
145    /// holds a `&str` never pays for that.
146    #[must_use]
147    pub const fn text(value: &'a str) -> Self {
148        Self::Text(Cow::Borrowed(value))
149    }
150
151    /// The id's text, or `None` for a numeric id.
152    ///
153    /// The borrowed string an [`Id`](crate::domain::RequestId) is built from.
154    #[must_use]
155    pub fn as_text(&self) -> Option<&str> {
156        match self {
157            Self::Text(value) => Some(value.as_ref()),
158            Self::Number(_) => None,
159        }
160    }
161
162    /// The numeric id, or `None` for a string id.
163    #[must_use]
164    pub const fn as_number(&self) -> Option<i64> {
165        match *self {
166            Self::Number(value) => Some(value),
167            Self::Text(_) => None,
168        }
169    }
170
171    /// The id as it appears on the wire, as JSON.
172    ///
173    /// A string is written with its quotes, so the result can be written into
174    /// a response without the caller having to know which arm it got.
175    #[must_use]
176    pub fn to_json(&self) -> String {
177        match self {
178            Self::Number(value) => value.to_string(),
179            // `format!("{value:?}")` is the obvious one-liner and it is wrong.
180            // `Debug` for `str` is `escape_debug`, which writes a control
181            // character as `\u{7f}` — Rust syntax, and not a legal JSON
182            // escape, which is `\u007f`. An id containing DEL would produce a
183            // response that is not JSON at all, and the peer would see a parse
184            // error rather than its own id. serde_json escapes to JSON.
185            Self::Text(value) => serde_json::to_string(value.as_ref()).unwrap_or_default(),
186        }
187    }
188}
189
190/// Why a wire id is not an id.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
192pub enum WireIdError {
193    /// The value was neither a number nor a string.
194    #[error("a JSON-RPC id must be a number or a string")]
195    NotAnId,
196    /// The number had a fractional part.
197    #[error("a JSON-RPC id must not contain fractional parts")]
198    NotAnInteger,
199}
200
201/// An error object, borrowed.
202///
203/// No `PartialEq`: `RawValue` does not implement it, because the question
204/// "are these the same JSON?" has two honest answers (equal as trees, equal as
205/// bytes) and `serde_json` declines to pick one. Callers that need it compare
206/// `.get()`, which is the byte-exact answer and the one the passthrough path
207/// wants anyway.
208#[derive(Debug, Clone)]
209pub struct WireError<'a> {
210    /// The numeric error code.
211    pub code: i64,
212    /// The human-readable message.
213    pub message: &'a str,
214    /// Anything else the sender attached, kept for the passthrough path.
215    pub data: Option<&'a RawValue>,
216}
217
218/// The `result` or `error` half of a reply.
219///
220/// No `PartialEq`, for the reason on [`WireError`].
221#[derive(Debug, Clone)]
222pub enum Outcome<'a> {
223    /// A successful reply.
224    Result(&'a RawValue),
225    /// A failed reply.
226    Error(WireError<'a>),
227}
228
229/// Which of the four JSON-RPC 2.0 shapes a message is.
230///
231/// Resolved by [`decode`] and stored, so no later stage re-derives it. A
232/// request is a message with a `method` **and** an `id`; a notification is a
233/// message with a `method` and no `id`; the two reply shapes have an `id` and
234/// no `method`.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236pub enum MessageKind {
237    /// `method` and `id`: expects a reply.
238    Request,
239    /// `method` and no `id`: no reply is ever sent.
240    Notification,
241    /// `id` and `result`.
242    Result,
243    /// `id` and `error`.
244    Error,
245}
246
247impl MessageKind {
248    /// Whether a reply is expected.
249    #[must_use]
250    pub const fn expects_reply(self) -> bool {
251        matches!(self, Self::Request)
252    }
253}
254
255/// One JSON-RPC message, borrowed from the frame buffer.
256///
257/// `kind` is the validated shape. The other fields are what the sender wrote,
258/// kept as they arrived — a `method` is `Some` on a request and a notification
259/// and `None` on a reply, and the two are the only way to tell those apart
260/// without consulting `kind`, which is why `kind` exists.
261///
262/// No `PartialEq`, for the reason on [`WireError`].
263#[derive(Debug, Clone)]
264pub struct Envelope<'a> {
265    pub(crate) kind: MessageKind,
266    pub(crate) id: Option<WireId<'a>>,
267    pub(crate) method: Option<Method<'a>>,
268    pub(crate) params: Option<&'a RawValue>,
269    pub(crate) outcome: Option<Outcome<'a>>,
270}
271
272impl<'a> Envelope<'a> {
273    /// The shape this message was decoded as.
274    #[must_use]
275    pub const fn kind(&self) -> MessageKind {
276        self.kind
277    }
278
279    /// The id, absent on a notification.
280    ///
281    /// By reference: an id whose text had to be unescaped owns a `String`, and
282    /// returning it by value would clone that on every read. The common
283    /// unescaped case borrows, so a clone there would be a refcount bump on
284    /// nothing — either way the caller only wanted to look at it.
285    #[must_use]
286    pub const fn id(&self) -> Option<&WireId<'a>> {
287        self.id.as_ref()
288    }
289
290    /// The method, absent on a reply.
291    #[must_use]
292    pub const fn method(&self) -> Option<&Method<'a>> {
293        self.method.as_ref()
294    }
295
296    /// The method as a plain string, whichever arm of the `Cow` it is.
297    #[must_use]
298    pub fn method_str(&self) -> Option<&str> {
299        self.method.as_deref()
300    }
301
302    /// The params, still undecoded.
303    ///
304    /// Borrowed raw JSON on purpose: the concrete type depends on the method
305    /// and the negotiated revision, and neither is known until after routing.
306    /// Decoding here would mean decoding every frame's params even for methods
307    /// that are about to be refused for not existing.
308    #[must_use]
309    pub const fn params(&self) -> Option<&'a RawValue> {
310        self.params
311    }
312
313    /// The reply half, present only on a `Result` or an `Error`.
314    #[must_use]
315    pub const fn outcome(&self) -> Option<&Outcome<'a>> {
316        self.outcome.as_ref()
317    }
318
319    /// Whether a reply is expected.
320    #[must_use]
321    pub const fn expects_reply(&self) -> bool {
322        self.kind.expects_reply()
323    }
324
325    /// The method and id together, for the router.
326    ///
327    /// Returns `None` for a reply shape, which has no method to route.
328    #[must_use]
329    pub fn routeable(&self) -> Option<(&Method<'a>, Option<&WireId<'a>>)> {
330        match self.kind {
331            MessageKind::Request | MessageKind::Notification => Some((
332                self.method.as_ref().unwrap_or(&Method::Borrowed("")),
333                self.id.as_ref(),
334            )),
335            MessageKind::Result | MessageKind::Error => None,
336        }
337    }
338}
339
340/// A whole frame: one message, or a batch of them.
341///
342/// The batch arm is recognised here rather than in a caller because a JSON
343/// array is a different *protocol* question from anything else on this path:
344/// `2025-06-18` removed batching, and the decision belongs to the session's
345/// profile, not to the decoder. So the decoder reports the shape and leaves
346/// the policy to [`crate::application::jsonrpc::BatchPolicy`].
347#[derive(Debug, Clone)]
348pub enum Frame<'a> {
349    /// A single message.
350    Single(Envelope<'a>),
351    /// A batch, in the order the sender wrote it.
352    Batch(Vec<Envelope<'a>>),
353}
354
355impl<'a> Frame<'a> {
356    /// Every message in the frame, in order.
357    #[must_use]
358    pub fn envelopes(&self) -> &[Envelope<'a>] {
359        match self {
360            Self::Single(envelope) => std::slice::from_ref(envelope),
361            Self::Batch(envelopes) => envelopes,
362        }
363    }
364
365    /// Every message in the frame, in order, moving each one out.
366    ///
367    /// The owned counterpart to [`envelopes`](Self::envelopes). Routing needs
368    /// it: the id of a request has to be moved into the reply, and an id whose
369    /// text needed unescaping owns a `String`, so routing cannot borrow.
370    #[must_use]
371    pub fn into_envelopes(self) -> Vec<Envelope<'a>> {
372        match self {
373            Self::Single(envelope) => vec![envelope],
374            Self::Batch(envelopes) => envelopes,
375        }
376    }
377
378    /// Whether this frame is a batch.
379    #[must_use]
380    pub const fn is_batch(&self) -> bool {
381        matches!(self, Self::Batch(_))
382    }
383}
384
385/// Why a frame could not be decoded.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
387pub enum DecodeError {
388    /// The bytes were not JSON at all (`-32700`).
389    #[error("the frame is not valid JSON")]
390    Json,
391    /// The frame was JSON but not a JSON-RPC message (`-32600`).
392    ///
393    /// This is the single bucket for every shape problem: not an object, a
394    /// missing or wrong `jsonrpc` member, a `method` next to a `result`, an id
395    /// that is not a number or a string. The specification gives all of them
396    /// the same code, and a peer gets nothing useful from a more precise one.
397    #[error("{0}")]
398    InvalidRequest(&'static str),
399    /// A batch frame was empty (`-32600`).
400    #[error("a batch must contain at least one message")]
401    EmptyBatch,
402}
403
404impl DecodeError {
405    /// The JSON-RPC code this failure must be reported as.
406    #[must_use]
407    pub const fn code(self) -> crate::domain::ir::IrErrorCode {
408        match self {
409            Self::Json => crate::domain::ir::IrErrorCode::ParseError,
410            Self::InvalidRequest(_) | Self::EmptyBatch => {
411                crate::domain::ir::IrErrorCode::InvalidRequest
412            }
413        }
414    }
415}
416
417/// The borrowed wire shape, decoded once by serde and then classified.
418///
419/// Nothing here denies unknown fields: a message this build does not
420/// understand still has to reach the passthrough path, and `deny_unknown_fields`
421/// would turn every forward-compatible extension into a parse error.
422#[derive(Deserialize)]
423struct WireMessage<'a> {
424    jsonrpc: &'a str,
425    /// An absent `id` and an explicit `"id":null` must stay distinguishable.
426    ///
427    /// A plain `Option<&RawValue>` cannot manage that: serde maps `null` to
428    /// `None`, so `{"jsonrpc":"2.0","id":null,"method":"ping"}` would decode
429    /// as a *notification* and the request would never be answered. A bare
430    /// `#[serde(default)]` does not help either, and neither does
431    /// `Option<Option<_>>` — `Option`'s own `Deserialize` is what collapses
432    /// the `null`. `deserialize_with` bypasses it: the function runs only when
433    /// the member is present, and it always returns `Some`, so `"id":null`
434    /// arrives as the raw text `null` and `validate` refuses it.
435    #[serde(borrow, default, deserialize_with = "present_member")]
436    id: Option<&'a RawValue>,
437    /// A method name may contain an escape, and `serde_json` will not lend a
438    /// `&str` for one — the same limit the id hits. It matters more here: a
439    /// peer may spell any method with any legal escaping, so `tools/list`
440    /// is `tools/list` and must route as it. Decoding the member as a
441    /// `&RawValue` and unescaping on the way out keeps every legal spelling
442    /// of a method working; a plain `&str` would refuse the whole frame as
443    /// `-32600` over a difference in spelling.
444    #[serde(borrow, default)]
445    method: Option<&'a RawValue>,
446    /// Also `present_member`, for the opposite reason to `id`.
447    ///
448    /// A JSON-RPC result may legitimately be `null`, and MCP leans on it — a
449    /// `tools/call` whose content is empty, or a handler that returns nothing,
450    /// both send `"result": null`. With a plain `Option` that collapses to
451    /// `None`, the message looks like it has neither a result nor an error, and
452    /// `{"jsonrpc":"2.0","id":1,"result":null}` is refused as `-32600`. The
453    /// peer did nothing wrong; the decode threw away the difference between
454    /// "no result" and "a result of null", which are different statements.
455    #[serde(borrow, default)]
456    params: Option<&'a RawValue>,
457    #[serde(borrow, default, deserialize_with = "present_member")]
458    result: Option<&'a RawValue>,
459    #[serde(borrow, default)]
460    error: Option<WireErrorRaw<'a>>,
461}
462
463/// The method name, borrowed from the frame unless it needed unescaping.
464///
465/// Mirrors [`WireId::from_raw`] for the reason given on [`WireId`]: a name
466/// with an escape cannot be borrowed, and a peer is allowed to send one.
467fn method_name<'a>(raw: &'a RawValue) -> Method<'a> {
468    let text = raw.get();
469    if let Ok(borrowed) = serde_json::from_str::<&'a str>(text) {
470        return Method::Borrowed(borrowed);
471    }
472    // Unescaping a method name cannot fail: `RawValue` is already a
473    // syntactically complete JSON value, so the only thing that can go wrong
474    // is the borrow, which is what the line above just found. The empty name
475    // is the unreachable fallback, and it is unreachable rather than chosen:
476    // a `Method` has to hold something.
477    serde_json::from_str::<String>(text).map_or(Method::Borrowed(""), Method::Owned)
478}
479
480/// Decodes a member that is present, keeping `null` as a value.
481///
482/// `default` handles the absent case and this handles the present one, so the
483/// two never meet. The result is always `Some` — the function is not called at
484/// all for a missing member, and a called function means the member was there.
485fn present_member<'de, 'a, D>(deserializer: D) -> Result<Option<&'a RawValue>, D::Error>
486where
487    D: serde::Deserializer<'de>,
488    'de: 'a,
489{
490    Ok(Some(serde::Deserialize::deserialize(deserializer)?))
491}
492
493/// The wire form of an error object, before the code is checked.
494#[derive(Deserialize)]
495struct WireErrorRaw<'a> {
496    code: i64,
497    #[serde(borrow)]
498    message: &'a str,
499    #[serde(borrow)]
500    data: Option<&'a RawValue>,
501}
502
503/// A top-level frame: one message or an array of them.
504///
505/// Not `#[serde(untagged)]`. An untagged enum deserialises through serde's
506/// private owned `Content` buffer, so no field can borrow from the input — the
507/// whole frame is materialised as a value tree and then read back out of it,
508/// which is both the reason the borrowed view could not compile and the
509/// intermediate value tree this path is supposed to avoid. The discriminator
510/// is a single byte, so reading it is cheaper than buffering for it.
511fn first_non_whitespace(input: &[u8]) -> Option<u8> {
512    input
513        .iter()
514        .find(|byte| !byte.is_ascii_whitespace())
515        .copied()
516}
517
518impl<'a> From<WireMessage<'a>> for Envelope<'a> {
519    fn from(wire: WireMessage<'a>) -> Self {
520        let id = wire.id.and_then(|raw| WireId::from_raw(raw).ok());
521        let outcome = match (wire.result, wire.error) {
522            (Some(result), None) => Some(Outcome::Result(result)),
523            (None, Some(error)) => Some(Outcome::Error(WireError {
524                code: error.code,
525                message: error.message,
526                data: error.data,
527            })),
528            _ => None,
529        };
530        // The four-way choice, made once. `classify` is total over everything
531        // that got this far; anything it rejects was already refused by
532        // `decode` before an `Envelope` was built.
533        let kind = classify(wire.method.is_some(), wire.id.is_some(), outcome.as_ref());
534        Self {
535            kind,
536            id,
537            method: wire.method.map(method_name),
538            params: wire.params,
539            outcome,
540        }
541    }
542}
543
544/// Resolves the four-way shape, defaulting the two shapes that cannot both be
545/// false.
546///
547/// A message with no `method` and no reply payload is not a JSON-RPC message at
548/// all, and `decode` refuses it before this runs. Treating it as a `Result`
549/// here is the only choice that keeps this function total, and the default is
550/// unreachable rather than meaningful.
551const fn classify(has_method: bool, has_id: bool, outcome: Option<&Outcome<'_>>) -> MessageKind {
552    match (has_method, has_id) {
553        (true, true) => MessageKind::Request,
554        (true, false) => MessageKind::Notification,
555        (false, _) => match outcome {
556            Some(Outcome::Error(_)) => MessageKind::Error,
557            Some(Outcome::Result(_)) | None => MessageKind::Result,
558        },
559    }
560}
561
562/// Decodes one frame from bytes, borrowing from `input`.
563///
564/// The bytes are parsed exactly once. `serde_json` walks the document and the
565/// borrowed fields point straight into `input`, so no sub-tree is
566/// re-serialised or re-parsed on the way out.
567///
568/// # Errors
569///
570/// [`DecodeError::Json`] if the bytes are not JSON, or the top-level value is
571/// neither an object nor an array. Otherwise
572/// [`DecodeError::InvalidRequest`] for every shape problem, and
573/// [`DecodeError::EmptyBatch`] for an empty array.
574///
575/// # Examples
576///
577/// ```
578/// use protocol_droid::domain::jsonrpc::{DecodeError, Frame, MessageKind, decode};
579///
580/// let frame = decode(br#"{"jsonrpc":"2.0","id":7,"method":"ping"}"#)
581///     .expect("a well-formed request decodes");
582/// let Frame::Single(envelope) = frame else {
583///     panic!("a lone object is not a batch");
584/// };
585/// assert_eq!(envelope.kind(), MessageKind::Request);
586/// assert_eq!(envelope.method_str(), Some("ping"));
587/// ```
588pub fn decode(input: &[u8]) -> Result<Frame<'_>, DecodeError> {
589    match first_non_whitespace(input) {
590        Some(b'[') => {
591            let messages: Vec<WireMessage<'_>> =
592                serde_json::from_slice(input).map_err(|error| map_json_error(&error))?;
593            if messages.is_empty() {
594                return Err(DecodeError::EmptyBatch);
595            }
596            let mut out = Vec::with_capacity(messages.len());
597            for message in &messages {
598                validate(message)?;
599                out.push(Envelope::from(clone_shape(message)));
600            }
601            Ok(Frame::Batch(out))
602        }
603        // `{` is a single message. Anything else is well-formed JSON that is
604        // not a JSON-RPC message, and the spec answers that with -32600 rather
605        // than -32700 — so the deserialise is allowed to fail and the error is
606        // reclassified below.
607        Some(b'{') => {
608            let message: WireMessage<'_> =
609                serde_json::from_slice(input).map_err(|error| map_json_error(&error))?;
610            validate(&message)?;
611            Ok(Frame::Single(message.into()))
612        }
613        Some(_) => Err(DecodeError::InvalidRequest(
614            "the frame is not an object or an array",
615        )),
616        // Empty or all whitespace: not JSON at all.
617        None => Err(DecodeError::Json),
618    }
619}
620
621/// Re-labels a serde failure by the code the specification assigns to it.
622///
623/// The distinction is not cosmetic: a peer that sent broken bytes needs
624/// `-32700` and a peer that sent a well-formed number where a message belongs
625/// needs `-32600`, and collapsing them into one tells the sender to fix the
626/// wrong thing.
627fn map_json_error(error: &serde_json::Error) -> DecodeError {
628    if error.is_syntax() || error.is_eof() {
629        DecodeError::Json
630    } else {
631        DecodeError::InvalidRequest("the frame is not a valid JSON-RPC message")
632    }
633}
634
635/// Copies the borrowed shape out of a batch element.
636///
637/// [`WireMessage`] holds no owned data, so this is a field-by-field copy rather
638/// than a clone of anything heap-allocated. It exists because `From` takes
639/// ownership and the batch loop borrows each element in turn.
640fn clone_shape<'a>(message: &WireMessage<'a>) -> WireMessage<'a> {
641    let WireMessage {
642        jsonrpc,
643        id,
644        method,
645        params,
646        result,
647        error,
648    } = message;
649    WireMessage {
650        jsonrpc,
651        id: *id,
652        method: *method,
653        params: *params,
654        result: *result,
655        error: error.as_ref().map(|error| WireErrorRaw {
656            code: error.code,
657            message: error.message,
658            data: error.data,
659        }),
660    }
661}
662
663/// The shape rules, applied before an `Envelope` exists.
664///
665/// All of them answer `-32600`; the reason string is for the log line and the
666/// caller, and never reaches the wire, because the wire gets the code.
667fn validate(message: &WireMessage<'_>) -> Result<(), DecodeError> {
668    if message.jsonrpc != JSONRPC_VERSION {
669        return Err(DecodeError::InvalidRequest("`jsonrpc` must be \"2.0\""));
670    }
671    let has_reply = message.result.is_some() || message.error.is_some();
672    match (message.method, has_reply) {
673        (Some(_), true) => {
674            return Err(DecodeError::InvalidRequest(
675                "a message cannot have both a method and a reply",
676            ));
677        }
678        (None, false) => {
679            return Err(DecodeError::InvalidRequest(
680                "a message needs a method or a reply",
681            ));
682        }
683        _ => {}
684    }
685    if let Some(raw_id) = message.id
686        && let Err(error) = WireId::from_raw(raw_id)
687    {
688        return Err(DecodeError::InvalidRequest(match error {
689            // Covers `"id": null` as well as `true`, an array, and an object.
690            // `null` is refused rather than treated as an absent id, because
691            // treating it as absent turns the sender's request into a
692            // notification and it would wait forever for a reply.
693            WireIdError::NotAnId => "an id must be a number or a string",
694            WireIdError::NotAnInteger => "a numeric id must not have a fractional part",
695        }));
696    }
697    Ok(())
698}
699
700#[cfg(test)]
701mod tests {
702    //! The envelope decoder, against the exact codes the specification assigns.
703    //!
704    //! # The three cases the task names
705    //!
706    //! Malformed JSON, a wrong `jsonrpc` version, and a missing id on a request
707    //! each have to produce a specific code, and all three are asserted by their
708    //! code rather than by "it failed". A test that only checks that decoding fails
709    //! passes against an implementation that returns the wrong code for every input,
710    //! which is the failure a peer cannot act on.
711    //!
712    //! `assert_matches!` rather than `assert!(matches!(..))` so a failure prints the
713    //! actual variant instead of `assertion failed`.
714    //!
715    //! These live in the module rather than `tests/` because the decoder is
716    //! domain API: an integration test sees the crate as a dependent, which is
717    //! the right way to test the *public* surface but leaves the compiler free
718    //! to call a public-but-unused item dead when it builds this crate as a
719    //! test binary. The properties that genuinely need a dependent's view of
720    //! the crate live in `tests/jsonrpc_fuzz.rs`.
721    // Test code unwraps deliberately: the strict profile exists to keep
722    // production paths fallible, and a test that cannot unwrap cannot assert.
723    // The messages name the failing precondition, which is the useful part.
724    #![expect(
725        clippy::panic,
726        clippy::unwrap_used,
727        reason = "tests assert on fallible calls and panic with the failing precondition; propagating errors would replace a clear failure message with noise"
728    )]
729
730    //! The envelope decoder, against the exact codes the specification assigns.
731    //!
732    //! # The three cases the task names
733    //!
734    //! Malformed JSON, a wrong `jsonrpc` version, and a missing id on a request
735    //! each have to produce a specific code, and all three are asserted by their
736    //! code rather than by "it failed". A test that only checks that decoding fails
737    //! passes against an implementation that returns the wrong code for every input,
738    //! which is the failure a peer cannot act on.
739    //!
740    //! `assert_matches!` rather than `assert!(matches!(..))` so a failure prints the
741    //! actual variant instead of `assertion failed`.
742
743    use crate::domain::RequestId;
744    use crate::domain::ir::IrErrorCode;
745    use crate::domain::jsonrpc::{
746        DecodeError, Envelope, Frame, MessageKind, Method, Outcome, WireId, WireIdError, decode,
747    };
748    use assert_matches::assert_matches;
749
750    type TestError = Box<dyn std::error::Error>;
751
752    fn single(input: &[u8]) -> Result<Envelope<'_>, TestError> {
753        let frame = decode(input)?;
754        match frame {
755            Frame::Single(envelope) => Ok(envelope),
756            Frame::Batch(_) => Err("expected a single message, got a batch".into()),
757        }
758    }
759
760    fn first(text: &[u8]) -> Result<Envelope<'_>, TestError> {
761        single(text)
762    }
763
764    #[test]
765    fn a_request_carries_a_method_and_an_id() -> Result<(), TestError> {
766        let envelope = first(r#"{"jsonrpc":"2.0","id":1,"method":"tools/call"}"#.as_bytes())?;
767        assert_eq!(envelope.kind(), MessageKind::Request);
768        assert_eq!(envelope.method_str(), Some("tools/call"));
769        assert_eq!(envelope.id(), Some(&WireId::Number(1)));
770        assert!(envelope.expects_reply());
771        assert!(envelope.outcome().is_none());
772        Ok(())
773    }
774
775    #[test]
776    fn a_notification_has_no_id_and_expects_nothing() -> Result<(), TestError> {
777        let envelope =
778            first(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#.as_bytes())?;
779        assert_eq!(envelope.kind(), MessageKind::Notification);
780        assert_eq!(envelope.id(), None);
781        assert!(!envelope.expects_reply());
782        Ok(())
783    }
784
785    #[test]
786    fn a_result_and_an_error_are_both_replies() -> Result<(), TestError> {
787        let result = first(r#"{"jsonrpc":"2.0","id":1,"result":{}}"#.as_bytes())?;
788        assert_eq!(result.kind(), MessageKind::Result);
789        assert!(!result.expects_reply());
790        assert_matches!(result.outcome(), Some(Outcome::Result(_)));
791
792        let error = first(
793            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"nope"}}"#.as_bytes(),
794        )?;
795        assert_eq!(error.kind(), MessageKind::Error);
796        assert_matches!(
797            error.outcome(),
798            Some(Outcome::Error(wire)) if wire.code == -32601 && wire.message == "nope"
799        );
800        Ok(())
801    }
802
803    #[test]
804    fn malformed_json_is_parse_error() {
805        let error = decode(b"{\"jsonrpc\":")
806            .map(|frame| format!("{frame:?}"))
807            .unwrap_err();
808        assert_eq!(error, DecodeError::Json);
809        assert_eq!(error.code(), IrErrorCode::ParseError);
810        assert_eq!(error.code().code(), -32700);
811    }
812
813    #[test]
814    fn a_wrong_jsonrpc_version_is_invalid_request() {
815        let error = first(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#.as_bytes()).unwrap_err();
816        assert_eq!(
817            error.downcast_ref::<DecodeError>(),
818            Some(&DecodeError::InvalidRequest("`jsonrpc` must be \"2.0\"")),
819        );
820        assert_eq!(
821            decode(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#.as_bytes())
822                .unwrap_err()
823                .code(),
824            IrErrorCode::InvalidRequest,
825        );
826        assert_eq!(
827            IrErrorCode::InvalidRequest.code(),
828            -32600,
829            "the code a peer switches on must be the spec's, not merely the variant's name"
830        );
831    }
832
833    #[test]
834    fn a_missing_id_on_a_request_is_invalid_request() {
835        // A message with a `method` and no `id` is a *notification*, which is
836        // legal, so this cannot be expressed as a malformed frame. The way to ask
837        // whether it is a valid request is to route it, and T09's router is where
838        // a caller learns it will never be answered. See
839        // `a_notification_is_never_a_request` in `jsonrpc_router.rs`.
840        let frame = decode(br#"{"jsonrpc":"2.0","method":"ping"}"#);
841        let Ok(Frame::Single(envelope)) = frame else {
842            panic!("a method without an id is still a well-formed message");
843        };
844        assert_eq!(envelope.kind(), MessageKind::Notification);
845        assert_eq!(envelope.id(), None);
846    }
847
848    #[test]
849    fn a_method_beside_a_result_is_invalid_request() {
850        assert_eq!(
851            decode(br#"{"jsonrpc":"2.0","id":1,"method":"ping","result":{}}"#).unwrap_err(),
852            DecodeError::InvalidRequest("a message cannot have both a method and a reply"),
853        );
854    }
855
856    #[test]
857    fn a_message_with_neither_method_nor_reply_is_invalid_request() {
858        assert_eq!(
859            decode(br#"{"jsonrpc":"2.0","id":1}"#).unwrap_err(),
860            DecodeError::InvalidRequest("a message needs a method or a reply"),
861        );
862    }
863
864    #[test]
865    fn an_id_that_is_neither_number_nor_string_is_invalid_request() {
866        for input in [
867            r#"{"jsonrpc":"2.0","id":true,"method":"ping"}"#,
868            r#"{"jsonrpc":"2.0","id":null,"method":"ping"}"#,
869            r#"{"jsonrpc":"2.0","id":[1],"method":"ping"}"#,
870            r#"{"jsonrpc":"2.0","id":{"a":1},"method":"ping"}"#,
871        ] {
872            assert_eq!(
873                decode(input.as_bytes()).unwrap_err(),
874                DecodeError::InvalidRequest("an id must be a number or a string"),
875                "input was {input}",
876            );
877        }
878    }
879
880    #[test]
881    fn a_fractional_id_is_invalid_request() {
882        assert_eq!(
883            decode(br#"{"jsonrpc":"2.0","id":1.5,"method":"ping"}"#).unwrap_err(),
884            DecodeError::InvalidRequest("a numeric id must not have a fractional part"),
885        );
886    }
887
888    #[test]
889    fn an_id_outside_i64_is_refused_rather_than_wrapped() {
890        // `2^63` is a JSON integer but not an `i64`. Wrapping it would put a
891        // different request on the in-flight map than the peer asked for, and the
892        // collision is silent.
893        let error =
894            decode(br#"{"jsonrpc":"2.0","id":9223372036854775808,"method":"ping"}"#).unwrap_err();
895        assert_matches!(error, DecodeError::InvalidRequest(_));
896    }
897
898    #[test]
899    fn a_wire_id_becomes_an_owned_request_id_without_loss() -> Result<(), TestError> {
900        for (input, expected) in [
901            (
902                r#"{"jsonrpc":"2.0","id":42,"method":"ping"}"#,
903                RequestId::number(42),
904            ),
905            (
906                r#"{"jsonrpc":"2.0","id":"abc","method":"ping"}"#,
907                RequestId::text("abc"),
908            ),
909        ] {
910            let envelope = first(input.as_bytes())?;
911            let wire = envelope.id().ok_or("the envelope has an id")?;
912            let owned = match wire.as_number() {
913                Some(number) => RequestId::number(number),
914                None => RequestId::text(wire.as_text().ok_or("a text id has text")?),
915            };
916            assert_eq!(owned, expected, "input was {input}");
917        }
918        Ok(())
919    }
920
921    #[test]
922    fn an_id_writes_back_as_the_json_the_sender_sent() -> Result<(), TestError> {
923        let numeric = first(r#"{"jsonrpc":"2.0","id":42,"method":"ping"}"#.as_bytes())?;
924        assert_eq!(
925            numeric.id().ok_or("the envelope has an id")?.to_json(),
926            "42",
927        );
928
929        let text = first(r#"{"jsonrpc":"2.0","id":"abc","method":"ping"}"#.as_bytes())?;
930        assert_eq!(
931            text.id().ok_or("the envelope has an id")?.to_json(),
932            "\"abc\"",
933            "a string id must come back quoted, or a response would not be valid JSON"
934        );
935        Ok(())
936    }
937
938    #[test]
939    fn a_wire_id_rejects_what_the_spec_rejects() -> Result<(), TestError> {
940        let number = serde_json::value::to_raw_value(&1).map_err(TestError::from)?;
941        assert_matches!(WireId::from_raw(&number), Ok(WireId::Number(1)));
942
943        let text = serde_json::value::to_raw_value("x").map_err(TestError::from)?;
944        // `assert_matches!` would need a literal pattern, and a `Cow` arm has
945        // none that is readable. Equality is the better assertion here anyway:
946        // it is what tells a borrowed id from an unescaped one.
947        let id = WireId::from_raw(&text).map_err(TestError::from)?;
948        assert_eq!(id, WireId::text("x"), "an unescaped id borrows");
949        assert_matches!(id, WireId::Text(_));
950
951        let float = serde_json::value::to_raw_value(&1.5).map_err(TestError::from)?;
952        assert_eq!(WireId::from_raw(&float), Err(WireIdError::NotAnInteger));
953
954        let boolean = serde_json::value::to_raw_value(&true).map_err(TestError::from)?;
955        assert_eq!(WireId::from_raw(&boolean), Err(WireIdError::NotAnId));
956        Ok(())
957    }
958
959    #[test]
960    fn a_batch_is_recognised_and_ordered() -> Result<(), TestError> {
961        let input = br#"[
962            {"jsonrpc":"2.0","id":1,"method":"ping"},
963            {"jsonrpc":"2.0","method":"notifications/initialized"}
964        ]"#;
965        let frame = decode(input)?;
966        assert!(frame.is_batch());
967        let envelopes = frame.envelopes();
968        assert_eq!(envelopes.len(), 2);
969        assert_eq!(
970            envelopes.first().ok_or("no first")?.id(),
971            Some(&WireId::Number(1))
972        );
973        assert_eq!(
974            envelopes.get(1).ok_or("no second")?.kind(),
975            MessageKind::Notification,
976        );
977        Ok(())
978    }
979
980    #[test]
981    fn an_empty_batch_is_invalid_request() {
982        assert_eq!(decode(b"[]").unwrap_err(), DecodeError::EmptyBatch);
983        assert_eq!(
984            DecodeError::EmptyBatch.code(),
985            IrErrorCode::InvalidRequest,
986            "an empty batch is a malformed frame, not a valid one with nothing in it"
987        );
988    }
989
990    #[test]
991    fn a_batch_is_rejected_whole_rather_than_elementwise() {
992        // The second element is bad, so the frame is refused — the first element is
993        // never reached. A batch that answered what it could would have run half
994        // the work before discovering the error.
995        let input = br#"[{"jsonrpc":"2.0","id":1,"method":"ping"},{"jsonrpc":"1.0","id":2}]"#;
996        assert_eq!(
997            decode(input).unwrap_err(),
998            DecodeError::InvalidRequest("`jsonrpc` must be \"2.0\""),
999        );
1000    }
1001
1002    #[test]
1003    fn a_top_level_value_that_is_not_a_frame_is_invalid_request() {
1004        // Well-formed JSON that is not a JSON-RPC message. `-32600`, not `-32700`:
1005        // the bytes are fine, the message is not.
1006        for input in [b"42".as_slice(), b"\"hi\"".as_slice(), b"true".as_slice()] {
1007            assert_matches!(
1008                decode(input).unwrap_err(),
1009                DecodeError::InvalidRequest(_),
1010                "input was {:?}",
1011                String::from_utf8_lossy(input),
1012            );
1013        }
1014    }
1015
1016    #[test]
1017    fn empty_input_is_a_parse_error() {
1018        assert_eq!(decode(b"").unwrap_err(), DecodeError::Json);
1019        assert_eq!(decode(b"   \n ").unwrap_err(), DecodeError::Json);
1020    }
1021
1022    #[test]
1023    fn unknown_members_survive() -> Result<(), TestError> {
1024        // A field this build has never heard of must not fail the decode. The
1025        // bridge's contract is that a peer may send anything, and dropping or
1026        // rejecting an unknown member is the one way to make that contract
1027        // impossible to keep.
1028        let input = br#"{"jsonrpc":"2.0","id":1,"method":"ping","$future":{"a":[1,2]}}"#;
1029        let envelope = first(input.as_slice())?;
1030        assert_eq!(envelope.kind(), MessageKind::Request);
1031        Ok(())
1032    }
1033
1034    #[test]
1035    fn leading_whitespace_does_not_hide_the_frame_kind() -> Result<(), TestError> {
1036        let batch = decode(b"  \n\t [ {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"} ]")?;
1037        assert!(batch.is_batch());
1038        let single = first(b"  {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}".as_slice())?;
1039        assert_eq!(single.kind(), MessageKind::Request);
1040        Ok(())
1041    }
1042
1043    #[test]
1044    fn params_stay_undecoded_until_the_method_is_known() -> Result<(), TestError> {
1045        let input = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"x","arguments":{}}}"#;
1046        let envelope = first(input.as_slice())?;
1047        let params = envelope.params().ok_or("the envelope has params")?;
1048        // Still the bytes the peer sent, not a decoded struct: that is the whole
1049        // point of deferring, and asserting on the raw text is what proves it.
1050        assert_eq!(params.get(), r#"{"name":"x","arguments":{}}"#);
1051        Ok(())
1052    }
1053
1054    #[test]
1055    fn a_message_with_null_params_and_no_params_are_different() -> Result<(), TestError> {
1056        // `params: null` is written by some peers and means "no params"; omitting
1057        // it says the same thing. The two decode the same way here, which is the
1058        // only safe reading — a peer that sends `null` means no params, and
1059        // treating it as an error breaks a working client.
1060        let omitted = first(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.as_bytes())?;
1061        let null = first(r#"{"jsonrpc":"2.0","id":1,"method":"ping","params":null}"#.as_bytes())?;
1062        assert!(omitted.params().is_none(), "no params member at all");
1063        assert!(
1064            null.params().is_none(),
1065            "an explicit null is the same absence, not a params value of null"
1066        );
1067        Ok(())
1068    }
1069
1070    #[test]
1071    fn a_routeable_message_reports_its_method_and_id() -> Result<(), TestError> {
1072        let request = first(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.as_bytes())?;
1073        assert_eq!(
1074            request.routeable(),
1075            Some((&Method::Borrowed("ping"), Some(&WireId::Number(1))))
1076        );
1077
1078        let notification = first(r#"{"jsonrpc":"2.0","method":"ping"}"#.as_bytes())?;
1079        assert_eq!(
1080            notification.routeable(),
1081            Some((&Method::Borrowed("ping"), None))
1082        );
1083
1084        let reply = first(r#"{"jsonrpc":"2.0","id":1,"result":{}}"#.as_bytes())?;
1085        assert_eq!(reply.routeable(), None, "a reply has no method to route");
1086        Ok(())
1087    }
1088
1089    #[test]
1090    fn a_numeric_id_needs_no_allocation() -> Result<(), TestError> {
1091        // The claim is that reading a numeric id copies an `i64` out of the buffer.
1092        // The evidence is the type: `WireId::Number` is an `i64`, so constructing
1093        // one from `&str` text cannot have allocated a `String` on the way. This
1094        // test is here so the claim is checked from the outside as well — if
1095        // someone changes `WireId` to own its text, this stops compiling.
1096        let envelope = first(br#"{"jsonrpc":"2.0","id":9007199254740993,"method":"ping"}"#)?;
1097        let value = envelope
1098            .id()
1099            .and_then(WireId::as_number)
1100            .ok_or("a numeric id must stay a number")?;
1101        assert_eq!(
1102            value, 9_007_199_254_740_993,
1103            "and it must be exact past 2^53"
1104        );
1105        Ok(())
1106    }
1107}