Skip to content

fix(types): conform JSON-RPC parsing to spec by properly trimming whitespace and newlines #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an
keywords = ["json-rpc", "jsonrpc", "json"]
categories = ["web-programming::http-server", "web-programming::websocket"]

version = "0.3.3"
version = "0.3.4"
edition = "2021"
rust-version = "1.81"
authors = ["init4", "James Prestwich"]
Expand Down
143 changes: 127 additions & 16 deletions src/types/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,33 +60,144 @@ impl TryFrom<Bytes> for InboundData {
}
debug!("Parsing inbound data");

// Special-case a single request, rejecting invalid JSON.
if bytes.starts_with(b"{") {
let rv: &RawValue = serde_json::from_slice(bytes.as_ref())?;
// We set up the deserializer to read from the byte buffer.
let mut deserializer = serde_json::Deserializer::from_slice(&bytes);

let range = find_range!(bytes, rv.get());
// If we succesfully deser a batch, we can return it.
if let Ok(reqs) = Vec::<&RawValue>::deserialize(&mut deserializer) {
// `.end()` performs trailing charcter checks
deserializer.end()?;
let reqs = reqs
.into_iter()
.map(|raw| find_range!(bytes, raw.get()))
.collect();

return Ok(Self {
bytes,
reqs: vec![range],
single: true,
reqs,
single: false,
});
}

// Otherwise, parse the batch
let DeserHelper(reqs) = serde_json::from_slice(bytes.as_ref())?;
let reqs = reqs
.into_iter()
.map(|raw| find_range!(bytes, raw.get()))
.collect();
// If it's not a batch, it should be a single request.
let rv = <&RawValue>::deserialize(&mut deserializer)?;

// `.end()` performs trailing charcter checks
deserializer.end()?;

// If not a JSON object, return an error.
if !rv.get().starts_with("{") {
return Err(RequestError::UnexpectedJsonType);
}

let range = find_range!(bytes, rv.get());

Ok(Self {
bytes,
reqs,
single: false,
reqs: vec![range],
single: true,
})
}
}

#[derive(Debug, Deserialize)]
struct DeserHelper<'a>(#[serde(borrow)] Vec<&'a RawValue>);
#[cfg(test)]
mod test {
use super::*;

fn assert_invalid_json(batch: &'static str) {
let bytes = Bytes::from(batch);
let err = InboundData::try_from(bytes).unwrap_err();

assert!(matches!(err, RequestError::InvalidJson(_)));
}

#[test]
fn test_deser_batch() {
let batch = r#"[
{"id": 1, "method": "foo", "params": [1, 2, 3]},
{"id": 2, "method": "bar", "params": [4, 5, 6]}
]"#;

let bytes = Bytes::from(batch);
let batch = InboundData::try_from(bytes).unwrap();

assert_eq!(batch.len(), 2);
assert!(!batch.single());
}

#[test]
fn test_deser_single() {
let single = r#"{"id": 1, "method": "foo", "params": [1, 2, 3]}"#;

let bytes = Bytes::from(single);
let batch = InboundData::try_from(bytes).unwrap();

assert_eq!(batch.len(), 1);
assert!(batch.single());
}

#[test]
fn test_deser_single_with_whitespace() {
let single = r#"

{"id": 1, "method": "foo", "params": [1, 2, 3]}

"#;

let bytes = Bytes::from(single);
let batch = InboundData::try_from(bytes).unwrap();

assert_eq!(batch.len(), 1);
assert!(batch.single());
}

#[test]
fn test_broken_batch() {
let batch = r#"[
{"id": 1, "method": "foo", "params": [1, 2, 3]},
{"id": 2, "method": "bar", "params": [4, 5, 6]
]"#;

assert_invalid_json(batch);
}

#[test]
fn test_junk_prefix() {
let batch = r#"JUNK[
{"id": 1, "method": "foo", "params": [1, 2, 3]},
{"id": 2, "method": "bar", "params": [4, 5, 6]}
]"#;

assert_invalid_json(batch);
}

#[test]
fn test_junk_suffix() {
let batch = r#"[
{"id": 1, "method": "foo", "params": [1, 2, 3]},
{"id": 2, "method": "bar", "params": [4, 5, 6]}
]JUNK"#;

assert_invalid_json(batch);
}

#[test]
fn test_invalid_utf8_prefix() {
let batch = r#"\xF1\x80[
{"id": 1, "method": "foo", "params": [1, 2, 3]},
{"id": 2, "method": "bar", "params": [4, 5, 6]}
]"#;

assert_invalid_json(batch);
}

#[test]
fn test_invalid_utf8_suffix() {
let batch = r#"[
{"id": 1, "method": "foo", "params": [1, 2, 3]},
{"id": 2, "method": "bar", "params": [4, 5, 6]}
]\xF1\x80"#;

assert_invalid_json(batch);
}
}
25 changes: 25 additions & 0 deletions src/types/req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ impl Request {

#[cfg(test)]
mod test {

use crate::types::METHOD_LEN_LIMIT;

use super::*;
Expand Down Expand Up @@ -236,4 +237,28 @@ mod test {

assert_eq!(size, METHOD_LEN_LIMIT + 1);
}

#[test]
fn test_with_linebreak() {
let bytes = Bytes::from_static(
r#"

{ "id": 1,
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0x4444d38c385d0969C64c4C8f996D7536d16c28B9", "latest"]
}

"#
.as_bytes(),
);
let req = Request::try_from(bytes).unwrap();

assert_eq!(req.id(), Some("1"));
assert_eq!(req.method(), r#"eth_getBalance"#);
assert_eq!(
req.params(),
r#"["0x4444d38c385d0969C64c4C8f996D7536d16c28B9", "latest"]"#
);
}
}
Loading