use std::io::BufRead;
use tokio::io::{AsyncBufReadExt, BufReader};
use crate::error::{Error, Result};
pub(crate) const DEFAULT_MAX_FRAME_LEN: usize = 4 * 1024 * 1024;
pub(crate) const DEFAULT_MAX_RESPONSE_FRAME_LEN: usize = 16 * 1024 * 1024;
pub(crate) enum InputFrame {
Line(String),
Undecodable,
}
pub(crate) fn decode_input_frame(mut raw: Vec<u8>) -> InputFrame {
if raw.last() == Some(&b'\n') {
raw.pop();
if raw.last() == Some(&b'\r') {
raw.pop();
}
}
match String::from_utf8(raw) {
Ok(line) => InputFrame::Line(line),
Err(_) => InputFrame::Undecodable,
}
}
pub(crate) struct FrameReader<R> {
reader: BufReader<R>,
buf: Vec<u8>,
max_frame_len: usize,
}
impl<R> FrameReader<R>
where
R: tokio::io::AsyncRead + Unpin,
{
#[cfg(test)]
pub(crate) fn new(reader: R) -> Self {
Self::with_max_len(reader, DEFAULT_MAX_FRAME_LEN)
}
pub(crate) fn with_max_len(reader: R, max_frame_len: usize) -> Self {
Self {
reader: BufReader::new(reader),
buf: Vec::new(),
max_frame_len,
}
}
pub(crate) fn set_max_len(&mut self, max_frame_len: usize) {
self.max_frame_len = max_frame_len;
}
pub(crate) async fn next_frame(&mut self) -> Result<Option<InputFrame>> {
loop {
let filled = self
.reader
.fill_buf()
.await
.map_err(|e| Error::Transport(format!("Failed to read input frame: {}", e)))?;
if filled.is_empty() {
break;
}
let (take, found_newline) = match filled.iter().position(|&b| b == b'\n') {
Some(pos) => (pos + 1, true),
None => (filled.len(), false),
};
if self.buf.len() + take > self.max_frame_len {
let size = self.buf.len() + take;
self.reader.consume(take);
self.buf.clear();
return Err(Error::FrameTooLarge {
size,
limit: self.max_frame_len,
});
}
self.buf.extend_from_slice(&filled[..take]);
self.reader.consume(take);
if found_newline {
break;
}
}
if self.buf.is_empty() {
return Ok(None);
}
Ok(Some(decode_input_frame(std::mem::take(&mut self.buf))))
}
}
pub(crate) fn read_frame_blocking<R: BufRead>(
reader: &mut R,
max_frame_len: usize,
) -> Result<Option<InputFrame>> {
let mut raw = Vec::new();
loop {
let filled = reader
.fill_buf()
.map_err(|e| Error::Transport(format!("Failed to read input frame: {}", e)))?;
if filled.is_empty() {
break;
}
let (take, found_newline) = match filled.iter().position(|&b| b == b'\n') {
Some(pos) => (pos + 1, true),
None => (filled.len(), false),
};
if raw.len() + take > max_frame_len {
let size = raw.len() + take;
reader.consume(take);
return Err(Error::FrameTooLarge {
size,
limit: max_frame_len,
});
}
raw.extend_from_slice(&filled[..take]);
reader.consume(take);
if found_newline {
break;
}
}
if raw.is_empty() {
return Ok(None);
}
Ok(Some(decode_input_frame(raw)))
}
pub(crate) fn clean_input_line(line: &str) -> &str {
line.strip_prefix('\u{feff}').unwrap_or(line).trim()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FrameClass {
Notification,
Response,
Request,
}
pub(crate) fn classify_frame(value: &serde_json::Value) -> FrameClass {
if !value.is_array() && value.get("id").is_none() {
return FrameClass::Notification;
}
if is_response_frame(value) {
return FrameClass::Response;
}
FrameClass::Request
}
pub(crate) fn is_response_frame(value: &serde_json::Value) -> bool {
!value.is_array()
&& value.get("method").is_none()
&& (value.get("result").is_some() || value.get("error").is_some())
}
pub(crate) fn correlating_id(value: &serde_json::Value) -> Option<crate::protocol::RequestId> {
match value.get("id")? {
serde_json::Value::String(id) => Some(crate::protocol::RequestId::String(id.clone())),
serde_json::Value::Number(id) => id.as_i64().map(crate::protocol::RequestId::Number),
_ => None,
}
}
pub(crate) fn error_response_id(
value: &serde_json::Value,
error: &crate::error::JsonRpcError,
) -> Option<crate::protocol::RequestId> {
const INVALID_REQUEST: i32 = -32600;
if error.code == INVALID_REQUEST {
return None;
}
correlating_id(value)
}
#[cfg(test)]
mod correlating_id_tests {
use super::*;
use crate::protocol::RequestId;
#[test]
fn both_json_rpc_id_types_are_read_off_the_frame() {
assert_eq!(
correlating_id(&serde_json::json!({"id": 42, "method": "tools/list"})),
Some(RequestId::Number(42))
);
assert_eq!(
correlating_id(&serde_json::json!({"id": "req-1", "method": "tools/list"})),
Some(RequestId::String("req-1".into()))
);
}
#[test]
fn a_batch_has_no_single_id_to_answer_with() {
assert_eq!(
correlating_id(&serde_json::json!([{"id": 1, "method": "tools/list"}])),
None
);
}
#[test]
fn a_notification_has_no_id() {
assert_eq!(
correlating_id(&serde_json::json!({"method": "notifications/initialized"})),
None
);
}
#[test]
fn an_id_of_the_wrong_type_is_not_an_id() {
for id in [
serde_json::json!(null),
serde_json::json!({"nested": 1}),
serde_json::json!([1]),
serde_json::json!(true),
] {
assert_eq!(
correlating_id(&serde_json::json!({"id": id, "method": "tools/list"})),
None,
"{id} is not a JSON-RPC id"
);
}
}
#[test]
fn a_fractional_id_does_not_truncate() {
assert_eq!(
correlating_id(&serde_json::json!({"id": 1.5, "method": "tools/list"})),
None
);
}
#[test]
fn an_invalid_request_answers_with_a_null_id() {
let frame = serde_json::json!({"jsonrpc": "2.0", "id": 6});
assert_eq!(
error_response_id(
&frame,
&crate::error::JsonRpcError::invalid_request("no method")
),
None
);
}
#[test]
fn a_semantic_failure_answers_with_the_id() {
let frame = serde_json::json!({"jsonrpc": "2.0", "id": 6, "method": "tools/list"});
for error in [
crate::error::JsonRpcError::invalid_params("bad _meta"),
crate::error::JsonRpcError::internal_error("boom"),
] {
assert_eq!(
error_response_id(&frame, &error),
Some(RequestId::Number(6)),
"code {} should correlate",
error.code
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_line(frame: Option<InputFrame>, expected: &str) {
match frame {
Some(InputFrame::Line(line)) => assert_eq!(line, expected),
Some(InputFrame::Undecodable) => panic!("{expected:?} must decode"),
None => panic!("expected a frame, got end of input"),
}
}
fn expect_frame_too_large(result: Result<Option<InputFrame>>) -> (usize, usize) {
match result {
Err(Error::FrameTooLarge { size, limit }) => (size, limit),
Err(other) => panic!("expected FrameTooLarge, got a different error: {other}"),
Ok(Some(InputFrame::Line(_))) => panic!("expected FrameTooLarge, got a line"),
Ok(Some(InputFrame::Undecodable)) => {
panic!("expected FrameTooLarge, got an undecodable frame")
}
Ok(None) => panic!("expected FrameTooLarge, got end of input"),
}
}
fn assert_undecodable(frame: Option<InputFrame>) {
assert!(
matches!(frame, Some(InputFrame::Undecodable)),
"expected an undecodable frame"
);
}
#[test]
fn decoding_strips_the_delimiter_in_both_line_endings() {
assert_line(Some(decode_input_frame(b"{}\n".to_vec())), "{}");
assert_line(Some(decode_input_frame(b"{}\r\n".to_vec())), "{}");
assert_line(Some(decode_input_frame(b"{}".to_vec())), "{}");
}
#[test]
fn decoding_rejects_bytes_rather_than_repairing_them() {
assert_undecodable(Some(decode_input_frame(vec![0xff, 0xfe, b'\n'])));
}
#[tokio::test]
async fn a_bad_frame_costs_only_itself() {
let input: &[u8] = b"\xff\xfe\n{\"id\":1}\n";
let mut frames = FrameReader::new(input);
assert_undecodable(frames.next_frame().await.unwrap());
assert_line(frames.next_frame().await.unwrap(), "{\"id\":1}");
assert!(
frames.next_frame().await.unwrap().is_none(),
"end of input must be reported once the frames are consumed"
);
}
#[test]
fn the_blocking_reader_treats_a_bad_frame_the_same_way() {
let mut input: &[u8] = b"\xff\xfe\n{\"id\":1}\n";
assert_undecodable(read_frame_blocking(&mut input, DEFAULT_MAX_FRAME_LEN).unwrap());
assert_line(
read_frame_blocking(&mut input, DEFAULT_MAX_FRAME_LEN).unwrap(),
"{\"id\":1}",
);
assert!(
read_frame_blocking(&mut input, DEFAULT_MAX_FRAME_LEN)
.unwrap()
.is_none()
);
}
#[test]
fn the_blocking_reader_is_bounded_the_same_way() {
let limit = 16;
let mut at_limit: &[u8] = b"123456789012345\n"; assert_line(
read_frame_blocking(&mut at_limit, limit).unwrap(),
"123456789012345",
);
let mut over_limit: &[u8] = b"1234567890123456\n"; assert_eq!(
expect_frame_too_large(read_frame_blocking(&mut over_limit, limit)),
(17, 16)
);
}
#[tokio::test]
async fn a_partial_frame_survives_a_cancelled_read() {
let (mut writer, reader) = tokio::io::duplex(256);
let mut frames = FrameReader::new(reader);
let frame = r#"{"jsonrpc":"2.0","id":2,"result":{"tools":[]}}"#;
tokio::io::AsyncWriteExt::write_all(&mut writer, &frame.as_bytes()[..10])
.await
.unwrap();
assert!(
tokio::time::timeout(std::time::Duration::from_millis(10), frames.next_frame())
.await
.is_err(),
"a partial frame must remain pending until its newline arrives"
);
tokio::io::AsyncWriteExt::write_all(&mut writer, &frame.as_bytes()[10..])
.await
.unwrap();
tokio::io::AsyncWriteExt::write_all(&mut writer, b"\n")
.await
.unwrap();
assert_line(frames.next_frame().await.unwrap(), frame);
}
#[tokio::test]
async fn an_oversized_frame_without_a_delimiter_is_rejected() {
let limit = 16;
let input: &[u8] = b"this line is much longer than the sixteen byte limit\n";
let mut frames = FrameReader::with_max_len(input, limit);
let (size, reported) = expect_frame_too_large(frames.next_frame().await);
assert_eq!(reported, limit);
assert!(size > limit, "the error must report the overshoot");
}
#[tokio::test]
async fn a_frame_one_byte_over_the_limit_is_rejected() {
let limit = 16;
let input: &[u8] = b"1234567890123456\n"; let mut frames = FrameReader::with_max_len(input, limit);
assert_eq!(expect_frame_too_large(frames.next_frame().await), (17, 16));
}
#[tokio::test]
async fn a_frame_exactly_at_the_limit_still_parses() {
let limit = 16;
let input: &[u8] = b"123456789012345\n"; let mut frames = FrameReader::with_max_len(input, limit);
assert_line(frames.next_frame().await.unwrap(), "123456789012345");
}
#[tokio::test]
async fn a_final_frame_without_a_delimiter_within_the_limit_still_parses_at_eof() {
let limit = 16;
let input: &[u8] = b"{}";
let mut frames = FrameReader::with_max_len(input, limit);
assert_line(frames.next_frame().await.unwrap(), "{}");
assert!(frames.next_frame().await.unwrap().is_none());
}
#[tokio::test]
async fn the_limit_is_enforced_incrementally_and_the_buffer_never_exceeds_it() {
let limit = 8;
let (mut writer, reader) = tokio::io::duplex(4);
let mut frames = FrameReader::with_max_len(reader, limit);
let write = tokio::spawn(async move {
let _ = tokio::io::AsyncWriteExt::write_all(&mut writer, b"123456789").await;
});
let result = tokio::time::timeout(std::time::Duration::from_secs(5), frames.next_frame())
.await
.expect("an oversized frame must fail rather than hang");
let (size, reported) = expect_frame_too_large(result);
assert_eq!(reported, limit);
assert!(
size <= limit + 4,
"buffer grew past one chunk beyond the limit: {size}"
);
write.abort();
}
#[tokio::test]
async fn the_limit_is_configurable_per_instance() {
let payload = b"{\"jsonrpc\":\"2.0\"}\n";
let mut generous = FrameReader::with_max_len(&payload[..], DEFAULT_MAX_FRAME_LEN);
assert_line(generous.next_frame().await.unwrap(), r#"{"jsonrpc":"2.0"}"#);
let mut strict = FrameReader::with_max_len(&payload[..], 4);
let (_, reported) = expect_frame_too_large(strict.next_frame().await);
assert_eq!(reported, 4);
}
#[test]
fn test_clean_input_line_no_bom() {
assert_eq!(
clean_input_line(r#"{"jsonrpc":"2.0"}"#),
r#"{"jsonrpc":"2.0"}"#
);
}
#[test]
fn test_clean_input_line_strips_leading_bom() {
let with_bom = "\u{feff}{\"jsonrpc\":\"2.0\"}";
assert_eq!(clean_input_line(with_bom), r#"{"jsonrpc":"2.0"}"#);
}
#[test]
fn test_clean_input_line_strips_bom_then_trims() {
let input = "\u{feff} {\"id\":1}\n";
assert_eq!(clean_input_line(input), r#"{"id":1}"#);
}
#[test]
fn test_clean_input_line_does_not_strip_internal_bom() {
let input = "{\"text\":\"hi\u{feff}there\"}";
assert_eq!(clean_input_line(input), input);
}
#[test]
fn test_clean_input_line_empty() {
assert_eq!(clean_input_line(""), "");
assert_eq!(clean_input_line("\u{feff}"), "");
assert_eq!(clean_input_line(" \n\t"), "");
}
#[test]
fn classify_frame_notification_has_no_id() {
let value = serde_json::json!({"jsonrpc": "2.0", "method": "notifications/initialized"});
assert_eq!(classify_frame(&value), FrameClass::Notification);
}
#[test]
fn classify_frame_response_has_id_no_method_and_result() {
let value = serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}});
assert_eq!(classify_frame(&value), FrameClass::Response);
}
#[test]
fn classify_frame_response_has_id_no_method_and_error() {
let value =
serde_json::json!({"jsonrpc": "2.0", "id": 1, "error": {"code": -1, "message": "x"}});
assert_eq!(classify_frame(&value), FrameClass::Response);
}
#[test]
fn classify_frame_request_has_id_and_method() {
let value = serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"});
assert_eq!(classify_frame(&value), FrameClass::Request);
}
#[test]
fn classify_frame_batch_array_is_always_request() {
let value = serde_json::json!([
{"jsonrpc": "2.0", "id": 1, "method": "a"},
{"jsonrpc": "2.0", "method": "b"},
]);
assert_eq!(classify_frame(&value), FrameClass::Request);
}
#[test]
fn classify_frame_id_present_but_null_is_not_a_notification() {
let value = serde_json::json!({"jsonrpc": "2.0", "id": null, "method": "tools/list"});
assert_eq!(classify_frame(&value), FrameClass::Request);
}
#[test]
fn classify_frame_id_present_but_null_can_still_be_a_response() {
let value = serde_json::json!({"jsonrpc": "2.0", "id": null, "result": {}});
assert_eq!(classify_frame(&value), FrameClass::Response);
}
#[test]
fn classify_frame_id_no_method_no_result_or_error_is_a_request_not_a_response() {
let value = serde_json::json!({"jsonrpc": "2.0", "id": 1});
assert_eq!(classify_frame(&value), FrameClass::Request);
}
#[test]
fn is_response_frame_matches_the_response_shape() {
assert!(is_response_frame(
&serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}})
));
assert!(is_response_frame(
&serde_json::json!({"jsonrpc": "2.0", "id": 1, "error": {"code": -1}})
));
}
#[test]
fn is_response_frame_rejects_a_request_shape() {
assert!(!is_response_frame(
&serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
));
assert!(!is_response_frame(
&serde_json::json!({"jsonrpc": "2.0", "id": 1})
));
}
#[test]
fn is_response_frame_does_not_itself_require_an_id() {
assert!(is_response_frame(&serde_json::json!({"result": {}})));
}
#[test]
fn is_response_frame_rejects_a_batch_array() {
assert!(!is_response_frame(&serde_json::json!([
{"result": {}},
])));
}
}