Skip to content

Commit 69b254b

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

1 file changed

Lines changed: 122 additions & 24 deletions

File tree

crates/rmcp/src/transport/async_rw.rs

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

3-
// use crate::schema::*;
4-
use futures::{SinkExt, StreamExt};
3+
use futures::SinkExt;
54
use serde::{Serialize, de::DeserializeOwned};
65
use thiserror::Error;
76
use tokio::{
8-
io::{AsyncRead, AsyncWrite},
7+
io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader},
98
sync::Mutex,
109
};
1110
use tokio_util::{
1211
bytes::{Buf, BufMut, BytesMut},
13-
codec::{Decoder, Encoder, FramedRead, FramedWrite},
12+
codec::{Decoder, Encoder, FramedWrite},
1413
};
1514

1615
use super::{IntoTransport, Transport};
@@ -47,8 +46,10 @@ where
4746
pub type TransportWriter<Role, W> = FramedWrite<W, JsonRpcMessageCodec<TxJsonRpcMessage<Role>>>;
4847

4948
pub struct AsyncRwTransport<Role: ServiceRole, R: AsyncRead, W: AsyncWrite> {
50-
read: FramedRead<R, JsonRpcMessageCodec<RxJsonRpcMessage<Role>>>,
49+
read: BufReader<R>,
50+
line_buf: Vec<u8>,
5151
write: Arc<Mutex<Option<TransportWriter<Role, W>>>>,
52+
_role: PhantomData<fn() -> Role>,
5253
}
5354

5455
impl<Role: ServiceRole, R, W> AsyncRwTransport<Role, R, W>
@@ -57,15 +58,17 @@ where
5758
W: Send + AsyncWrite + Unpin + 'static,
5859
{
5960
pub fn new(read: R, write: W) -> Self {
60-
let read = FramedRead::new(
61-
read,
62-
JsonRpcMessageCodec::<RxJsonRpcMessage<Role>>::default(),
63-
);
61+
let read = BufReader::new(read);
6462
let write = Arc::new(Mutex::new(Some(FramedWrite::new(
6563
write,
6664
JsonRpcMessageCodec::<TxJsonRpcMessage<Role>>::default(),
6765
))));
68-
Self { read, write }
66+
Self {
67+
read,
68+
line_buf: Vec::new(),
69+
write,
70+
_role: PhantomData,
71+
}
6972
}
7073
}
7174

@@ -116,15 +119,42 @@ where
116119
}
117120
}
118121

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| {
122+
async fn receive(&mut self) -> Option<RxJsonRpcMessage<Role>> {
123+
loop {
124+
self.line_buf.clear();
125+
match self.read.read_until(b'\n', &mut self.line_buf).await {
126+
Ok(0) => return None,
127+
Ok(_) => {}
128+
Err(e) => {
124129
tracing::error!("Error reading from stream: {}", e);
125-
})
126-
.ok()
127-
})
130+
return None;
131+
}
132+
}
133+
let line = without_carriage_return(
134+
self.line_buf.strip_suffix(b"\n").unwrap_or(&self.line_buf),
135+
);
136+
if line.is_empty() {
137+
continue;
138+
}
139+
match try_parse_with_compatibility::<RxJsonRpcMessage<Role>>(line, "receive") {
140+
Ok(Some(msg)) => return Some(msg),
141+
Ok(None) => continue,
142+
Err(JsonRpcMessageCodecError::Serde(e)) => {
143+
tracing::debug!("Parse error on incoming message: {e}");
144+
let mut write = self.write.lock().await;
145+
let framed = write.as_mut()?;
146+
let inner = framed.get_mut();
147+
if inner.write_all(PARSE_ERROR_RESPONSE).await.is_err()
148+
|| inner.flush().await.is_err()
149+
{
150+
return None;
151+
}
152+
}
153+
Err(e) => {
154+
tracing::error!("Error reading from stream: {}", e);
155+
return None;
156+
}
157+
}
128158
}
129159
}
130160

@@ -172,13 +202,17 @@ impl<T> JsonRpcMessageCodec<T> {
172202
}
173203

174204
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-
}
205+
s.strip_suffix(b"\r").unwrap_or(s)
180206
}
181207

208+
/// UTF-8 byte order mark. RFC 8259 §8.1 allows JSON parsers to ignore a leading BOM.
209+
const UTF8_BOM: &[u8; 3] = b"\xEF\xBB\xBF";
210+
211+
// Sent verbatim because `RequestId` has no `Null` variant, so we cannot
212+
// construct an `id: null` JsonRpcError through the typed codec.
213+
const PARSE_ERROR_RESPONSE: &[u8] =
214+
b"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,\"message\":\"Parse error\"},\"id\":null}\n";
215+
182216
/// Check if a method is a standard MCP method (request, response, or notification).
183217
/// This includes both requests and notifications defined in the MCP specification.
184218
///
@@ -247,6 +281,7 @@ fn try_parse_with_compatibility<T: serde::de::DeserializeOwned>(
247281
line: &[u8],
248282
context: &str,
249283
) -> Result<Option<T>, JsonRpcMessageCodecError> {
284+
let line = line.strip_prefix(UTF8_BOM.as_slice()).unwrap_or(line);
250285
if let Ok(line_str) = std::str::from_utf8(line) {
251286
match serde_json::from_slice(line) {
252287
Ok(item) => Ok(Some(item)),
@@ -406,7 +441,8 @@ impl<T: Serialize> Encoder<T> for JsonRpcMessageCodec<T> {
406441

407442
#[cfg(test)]
408443
mod test {
409-
use futures::{Sink, Stream};
444+
use futures::{Sink, Stream, StreamExt};
445+
use tokio_util::codec::FramedRead;
410446

411447
use super::*;
412448
fn from_async_read<T: DeserializeOwned, R: AsyncRead>(reader: R) -> impl Stream<Item = T> {
@@ -555,4 +591,66 @@ mod test {
555591

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

0 commit comments

Comments
 (0)