Skip to content

Commit 0d39c02

Browse files
committed
fix: reply -32700 on stdio parse errors instead of closing
1 parent 88df9af commit 0d39c02

1 file changed

Lines changed: 140 additions & 23 deletions

File tree

crates/rmcp/src/transport/async_rw.rs

Lines changed: 140 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
use std::{marker::PhantomData, sync::Arc};
22

33
// use crate::schema::*;
4-
use futures::{SinkExt, StreamExt};
4+
use futures::SinkExt;
55
use serde::{Serialize, de::DeserializeOwned};
66
use thiserror::Error;
77
use tokio::{
8-
io::{AsyncRead, AsyncWrite},
8+
io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader},
99
sync::Mutex,
1010
};
1111
use tokio_util::{
1212
bytes::{Buf, BufMut, BytesMut},
13-
codec::{Decoder, Encoder, FramedRead, FramedWrite},
13+
codec::{Decoder, Encoder, FramedWrite},
1414
};
1515

1616
use super::{IntoTransport, Transport};
@@ -47,8 +47,10 @@ where
4747
pub type TransportWriter<Role, W> = FramedWrite<W, JsonRpcMessageCodec<TxJsonRpcMessage<Role>>>;
4848

4949
pub struct AsyncRwTransport<Role: ServiceRole, R: AsyncRead, W: AsyncWrite> {
50-
read: FramedRead<R, JsonRpcMessageCodec<RxJsonRpcMessage<Role>>>,
50+
read: BufReader<R>,
51+
line_buf: Vec<u8>,
5152
write: Arc<Mutex<Option<TransportWriter<Role, W>>>>,
53+
_role: PhantomData<fn() -> Role>,
5254
}
5355

5456
impl<Role: ServiceRole, R, W> AsyncRwTransport<Role, R, W>
@@ -57,15 +59,17 @@ where
5759
W: Send + AsyncWrite + Unpin + 'static,
5860
{
5961
pub fn new(read: R, write: W) -> Self {
60-
let read = FramedRead::new(
61-
read,
62-
JsonRpcMessageCodec::<RxJsonRpcMessage<Role>>::default(),
63-
);
62+
let read = BufReader::new(read);
6463
let write = Arc::new(Mutex::new(Some(FramedWrite::new(
6564
write,
6665
JsonRpcMessageCodec::<TxJsonRpcMessage<Role>>::default(),
6766
))));
68-
Self { read, write }
67+
Self {
68+
read,
69+
line_buf: Vec::new(),
70+
write,
71+
_role: PhantomData,
72+
}
6973
}
7074
}
7175

@@ -116,15 +120,40 @@ where
116120
}
117121
}
118122

119-
fn receive(&mut self) -> impl Future<Output = Option<RxJsonRpcMessage<Role>>> {
120-
let next = self.read.next();
121-
async {
122-
next.await.and_then(|e| {
123-
e.inspect_err(|e| {
123+
async fn receive(&mut self) -> Option<RxJsonRpcMessage<Role>> {
124+
loop {
125+
self.line_buf.clear();
126+
match self.read.read_until(b'\n', &mut self.line_buf).await {
127+
Ok(0) => return None,
128+
Ok(_) => {}
129+
Err(e) => {
124130
tracing::error!("Error reading from stream: {}", e);
125-
})
126-
.ok()
127-
})
131+
return None;
132+
}
133+
}
134+
let line = without_line_terminator(&self.line_buf);
135+
if line.is_empty() {
136+
continue;
137+
}
138+
match try_parse_with_compatibility::<RxJsonRpcMessage<Role>>(line, "receive") {
139+
Ok(Some(msg)) => return Some(msg),
140+
Ok(None) => continue,
141+
Err(JsonRpcMessageCodecError::Serde(e)) => {
142+
tracing::debug!("Parse error on incoming message: {e}");
143+
let mut write = self.write.lock().await;
144+
let framed = write.as_mut()?;
145+
let inner = framed.get_mut();
146+
if inner.write_all(PARSE_ERROR_RESPONSE).await.is_err()
147+
|| inner.flush().await.is_err()
148+
{
149+
return None;
150+
}
151+
}
152+
Err(e) => {
153+
tracing::error!("Error reading from stream: {}", e);
154+
return None;
155+
}
156+
}
128157
}
129158
}
130159

@@ -172,13 +201,25 @@ impl<T> JsonRpcMessageCodec<T> {
172201
}
173202

174203
fn without_carriage_return(s: &[u8]) -> &[u8] {
175-
if let Some(&b'\r') = s.last() {
176-
&s[..s.len() - 1]
177-
} else {
178-
s
179-
}
204+
s.strip_suffix(b"\r").unwrap_or(s)
180205
}
181206

207+
fn without_line_terminator(s: &[u8]) -> &[u8] {
208+
without_carriage_return(s.strip_suffix(b"\n").unwrap_or(s))
209+
}
210+
211+
/// UTF-8 byte order mark. RFC 8259 §8.1 allows JSON parsers to ignore a leading BOM.
212+
const UTF8_BOM: &[u8; 3] = b"\xEF\xBB\xBF";
213+
214+
fn without_utf8_bom(s: &[u8]) -> &[u8] {
215+
s.strip_prefix(UTF8_BOM.as_slice()).unwrap_or(s)
216+
}
217+
218+
// Sent verbatim because `RequestId` has no `Null` variant, so we cannot
219+
// construct an `id: null` JsonRpcError through the typed codec.
220+
const PARSE_ERROR_RESPONSE: &[u8] =
221+
b"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,\"message\":\"Parse error\"},\"id\":null}\n";
222+
182223
/// Check if a method is a standard MCP method (request, response, or notification).
183224
/// This includes both requests and notifications defined in the MCP specification.
184225
///
@@ -247,6 +288,7 @@ fn try_parse_with_compatibility<T: serde::de::DeserializeOwned>(
247288
line: &[u8],
248289
context: &str,
249290
) -> Result<Option<T>, JsonRpcMessageCodecError> {
291+
let line = without_utf8_bom(line);
250292
if let Ok(line_str) = std::str::from_utf8(line) {
251293
match serde_json::from_slice(line) {
252294
Ok(item) => Ok(Some(item)),
@@ -406,7 +448,8 @@ impl<T: Serialize> Encoder<T> for JsonRpcMessageCodec<T> {
406448

407449
#[cfg(test)]
408450
mod test {
409-
use futures::{Sink, Stream};
451+
use futures::{Sink, Stream, StreamExt};
452+
use tokio_util::codec::FramedRead;
410453

411454
use super::*;
412455
fn from_async_read<T: DeserializeOwned, R: AsyncRead>(reader: R) -> impl Stream<Item = T> {
@@ -555,4 +598,78 @@ mod test {
555598

556599
println!("Standard notifications are preserved, non-standard are handled gracefully");
557600
}
601+
602+
#[tokio::test]
603+
async fn test_decode_strips_utf8_bom() {
604+
use futures::StreamExt;
605+
use tokio::io::BufReader;
606+
607+
// Valid JSON-RPC message preceded by a UTF-8 BOM (EF BB BF). Some Windows
608+
// tooling and editors prepend this; the codec should ignore it per RFC 8259 §8.1.
609+
let mut data = Vec::new();
610+
data.extend_from_slice(UTF8_BOM);
611+
data.extend_from_slice(br#"{"jsonrpc":"2.0","method":"ping","id":1}"#);
612+
data.push(b'\n');
613+
614+
let mut cursor = BufReader::new(&data[..]);
615+
let mut stream = from_async_read::<serde_json::Value, _>(&mut cursor);
616+
617+
let item = stream
618+
.next()
619+
.await
620+
.expect("should decode BOM-prefixed line");
621+
assert_eq!(
622+
item,
623+
serde_json::json!({"jsonrpc": "2.0", "method": "ping", "id": 1})
624+
);
625+
}
626+
627+
#[cfg(feature = "server")]
628+
async fn drive_parse_error_recovery() -> (RxJsonRpcMessage<crate::RoleServer>, Vec<u8>) {
629+
use tokio::io::AsyncReadExt;
630+
631+
use crate::{RoleServer, transport::Transport};
632+
633+
// Two paired streams: `server_io` is wrapped by the transport; the test
634+
// drives `client_io` to act as the peer.
635+
let (server_io, client_io) = tokio::io::duplex(4096);
636+
let (server_r, server_w) = tokio::io::split(server_io);
637+
let (mut client_r, mut client_w) = tokio::io::split(client_io);
638+
639+
let mut transport = AsyncRwTransport::<RoleServer, _, _>::new(server_r, server_w);
640+
641+
client_w
642+
.write_all(
643+
b"not json\n{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n",
644+
)
645+
.await
646+
.unwrap();
647+
648+
let received = transport
649+
.receive()
650+
.await
651+
.expect("transport should recover and yield the next valid message");
652+
653+
let mut reply = vec![0u8; PARSE_ERROR_RESPONSE.len()];
654+
client_r.read_exact(&mut reply).await.unwrap();
655+
656+
(received, reply)
657+
}
658+
659+
#[cfg(feature = "server")]
660+
#[tokio::test]
661+
async fn receive_yields_next_valid_message_after_parse_error() {
662+
let (received, _) = drive_parse_error_recovery().await;
663+
assert_eq!(
664+
serde_json::to_value(&received).unwrap()["method"],
665+
"notifications/initialized"
666+
);
667+
}
668+
669+
#[cfg(feature = "server")]
670+
#[tokio::test]
671+
async fn receive_emits_parse_error_response_to_peer() {
672+
let (_, reply) = drive_parse_error_recovery().await;
673+
assert_eq!(reply, PARSE_ERROR_RESPONSE);
674+
}
558675
}

0 commit comments

Comments
 (0)