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}