Skip to content

Commit f95a9ed

Browse files
authored
fix: allow missing id and missing params (#9)
* fix: missing id and missing params * lint: clippy * lint: clippy again * test: reorganize and break up * clean: test organization and ids * chore: bump to 0.1.2
1 parent 195b772 commit f95a9ed

File tree

6 files changed

+113
-57
lines changed

6 files changed

+113
-57
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an
55
keywords = ["json-rpc", "jsonrpc", "json"]
66
categories = ["web-programming::http-server", "web-programming::websocket"]
77

8-
version = "0.1.1"
8+
version = "0.1.2"
99
edition = "2021"
1010
rust-version = "1.81"
1111
authors = ["init4", "James Prestwich"]

src/pubsub/shared.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,12 @@ where
202202
break;
203203
};
204204

205-
let Ok(req) = Request::try_from(item) else {
206-
tracing::warn!("inbound request is malformatted");
207-
continue
205+
let req = match Request::try_from(item) {
206+
Ok(req) => req,
207+
Err(err) => {
208+
tracing::warn!(%err, "inbound request is malformatted");
209+
continue
210+
}
208211
};
209212

210213
let span = debug_span!("ipc request handling", id = req.id(), method = req.method());

src/types/req.rs

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub struct Request {
3131
///
3232
/// This field is generated by deserializing to a [`RawValue`] and then
3333
/// calculating the offset of the backing slice within the `bytes` field.
34-
id: Range<usize>,
34+
id: Option<Range<usize>>,
3535
/// A range of the `bytes` field that represents the method field of the
3636
/// JSON-RPC request.
3737
///
@@ -49,7 +49,7 @@ pub struct Request {
4949
///
5050
/// This field is generated by deserializing to a [`RawValue`] and then
5151
/// calculating the offset of the backing slice within the `bytes` field.
52-
params: Range<usize>,
52+
params: Option<Range<usize>>,
5353
}
5454

5555
impl core::fmt::Debug for Request {
@@ -67,11 +67,11 @@ impl core::fmt::Debug for Request {
6767
#[derive(serde::Deserialize)]
6868
struct DeserHelper<'a> {
6969
#[serde(borrow)]
70-
id: &'a RawValue,
70+
id: Option<&'a RawValue>,
7171
#[serde(borrow)]
7272
method: &'a RawValue,
7373
#[serde(borrow)]
74-
params: &'a RawValue,
74+
params: Option<&'a RawValue>,
7575
}
7676

7777
impl TryFrom<Bytes> for Request {
@@ -80,12 +80,19 @@ impl TryFrom<Bytes> for Request {
8080
fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
8181
let DeserHelper { id, method, params } = serde_json::from_slice(bytes.as_ref())?;
8282

83-
let id = find_range!(bytes, id.get());
84-
// Ensure the id is not too long
85-
let id_len = id.end - id.start;
86-
if id_len > ID_LEN_LIMIT {
87-
return Err(RequestError::IdTooLarge(id_len));
88-
}
83+
let id = if let Some(id) = id {
84+
let id = find_range!(bytes, id.get());
85+
86+
// Ensure the id is not too long
87+
let id_len = id.end - id.start;
88+
if id_len > ID_LEN_LIMIT {
89+
return Err(RequestError::IdTooLarge(id_len));
90+
}
91+
92+
Some(id)
93+
} else {
94+
None
95+
};
8996

9097
// Ensure method is a string, and not too long, and trim the quotes
9198
// from it
@@ -101,7 +108,7 @@ impl TryFrom<Bytes> for Request {
101108
return Err(RequestError::MethodTooLarge(method_len));
102109
}
103110

104-
let params = find_range!(bytes, params.get());
111+
let params = params.map(|params| find_range!(bytes, params.get()));
105112

106113
Ok(Self {
107114
bytes,
@@ -122,11 +129,20 @@ impl TryFrom<tokio_tungstenite::tungstenite::Utf8Bytes> for Request {
122129
}
123130

124131
impl Request {
125-
/// Return a reference to the serialized ID field.
132+
/// Return a reference to the serialized ID field. If the ID field is
133+
/// missing, this will return `"null"`, ensuring that response correctly
134+
/// have a null ID, as per [the JSON-RPC spec].
135+
///
136+
/// [the JSON-RPC spec]: https://www.jsonrpc.org/specification#response_object
126137
pub fn id(&self) -> &str {
127-
// SAFETY: `id` is guaranteed to be valid JSON,
128-
// and a valid slice of `bytes`.
129-
unsafe { core::str::from_utf8_unchecked(self.bytes.get_unchecked(self.id.clone())) }
138+
self.id
139+
.as_ref()
140+
.map(|range| {
141+
// SAFETY: `range` is guaranteed to be valid JSON, and a valid
142+
// slice of `bytes`.
143+
unsafe { core::str::from_utf8_unchecked(self.bytes.get_unchecked(range.clone())) }
144+
})
145+
.unwrap_or("null")
130146
}
131147

132148
/// Return an owned version of the serialized ID field.
@@ -161,9 +177,13 @@ impl Request {
161177

162178
/// Return a reference to the serialized params field.
163179
pub fn params(&self) -> &str {
164-
// SAFETY: `params` is guaranteed to be valid JSON,
165-
// and a valid slice of `bytes`.
166-
unsafe { core::str::from_utf8_unchecked(self.bytes.get_unchecked(self.params.clone())) }
180+
if let Some(range) = &self.params {
181+
// SAFETY: `range` is guaranteed to be valid JSON, and a valid
182+
// slice of `bytes`.
183+
unsafe { core::str::from_utf8_unchecked(self.bytes.get_unchecked(range.clone())) }
184+
} else {
185+
"null"
186+
}
167187
}
168188

169189
/// Deserialize the params field into a type.

tests/common/mod.rs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,63 @@ pub fn test_router() -> ajj::Router<()> {
2828

2929
/// Test clients
3030
pub trait TestClient {
31-
async fn send<S: serde::Serialize>(&mut self, method: &str, params: &S);
31+
fn next_id(&mut self) -> usize;
32+
33+
async fn send_raw<S: serde::Serialize>(&mut self, msg: &S);
34+
3235
async fn recv<D: serde::de::DeserializeOwned>(&mut self) -> D;
36+
37+
async fn send<S: serde::Serialize>(&mut self, method: &str, params: &S) -> usize {
38+
let id = self.next_id();
39+
self.send_raw(&serde_json::json!({
40+
"jsonrpc": "2.0",
41+
"id": id,
42+
"method": method,
43+
"params": params,
44+
}))
45+
.await;
46+
id
47+
}
3348
}
3449

3550
/// basic tests of the test router
3651
pub async fn basic_tests<T: TestClient>(mut client: T) {
37-
client.send("ping", &()).await;
52+
test_ping(&mut client).await;
3853

39-
let next: Value = client.recv().await;
40-
assert_eq!(
41-
next,
42-
serde_json::json!({"id": 0, "jsonrpc": "2.0", "result": "pong"})
43-
);
54+
test_double(&mut client).await;
55+
56+
test_notify(&mut client).await;
57+
58+
test_missing_id(&mut client).await;
59+
}
60+
61+
async fn test_missing_id<T: TestClient>(client: &mut T) {
62+
client
63+
.send_raw(&serde_json::json!(
64+
{"jsonrpc": "2.0", "method": "ping"}
65+
))
66+
.await;
4467

45-
client.send("double", &5).await;
4668
let next: Value = client.recv().await;
4769
assert_eq!(
4870
next,
49-
serde_json::json!({"id": 1, "jsonrpc": "2.0", "result": 10})
71+
serde_json::json!({
72+
"jsonrpc": "2.0",
73+
"result": "pong",
74+
"id": null,
75+
})
5076
);
77+
}
5178

52-
client.send("notify", &()).await;
79+
async fn test_notify<T: TestClient>(client: &mut T) {
80+
let id = client.send("notify", &()).await;
5381

5482
let now = std::time::Instant::now();
5583

5684
let next: Value = client.recv().await;
5785
assert_eq!(
5886
next,
59-
serde_json::json!({"id": 2, "jsonrpc": "2.0", "result": null})
87+
serde_json::json!({"id": id, "jsonrpc": "2.0", "result": null})
6088
);
6189

6290
let next: Value = client.recv().await;
@@ -66,3 +94,22 @@ pub async fn basic_tests<T: TestClient>(mut client: T) {
6694
serde_json::json!({"method": "notify", "result": "notified"})
6795
);
6896
}
97+
98+
async fn test_double<T: TestClient>(client: &mut T) {
99+
let id = client.send("double", &5).await;
100+
let next: Value = client.recv().await;
101+
assert_eq!(
102+
next,
103+
serde_json::json!({"id": id, "jsonrpc": "2.0", "result": 10})
104+
);
105+
}
106+
107+
async fn test_ping<T: TestClient>(client: &mut T) {
108+
let id = client.send("ping", &()).await;
109+
110+
let next: Value = client.recv().await;
111+
assert_eq!(
112+
next,
113+
serde_json::json!({"id": id, "jsonrpc": "2.0", "result": "pong"})
114+
);
115+
}

tests/ipc.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,24 +64,17 @@ impl IpcClient {
6464
async fn recv_inner(&mut self) -> serde_json::Value {
6565
self.recv_half.next().await.unwrap()
6666
}
67+
}
6768

69+
impl TestClient for IpcClient {
6870
fn next_id(&mut self) -> usize {
6971
let id = self.id;
7072
self.id += 1;
7173
id
7274
}
73-
}
7475

75-
impl TestClient for IpcClient {
76-
async fn send<S: serde::Serialize>(&mut self, method: &str, params: &S) {
77-
let id = self.next_id();
78-
self.send_inner(&serde_json::json!({
79-
"jsonrpc": "2.0",
80-
"id": id,
81-
"method": method,
82-
"params": params,
83-
}))
84-
.await;
76+
async fn send_raw<S: serde::Serialize>(&mut self, msg: &S) {
77+
self.send_inner(msg).await;
8578
}
8679

8780
async fn recv<D: serde::de::DeserializeOwned>(&mut self) -> D {

tests/ws.rs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ async fn serve_ws() -> ServerShutdown {
2020

2121
struct WsClient {
2222
socket: WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>,
23-
id: u64,
23+
id: usize,
2424
}
2525

2626
impl WsClient {
@@ -37,24 +37,17 @@ impl WsClient {
3737
_ => panic!("unexpected message type"),
3838
}
3939
}
40+
}
4041

41-
fn next_id(&mut self) -> u64 {
42+
impl TestClient for WsClient {
43+
fn next_id(&mut self) -> usize {
4244
let id = self.id;
4345
self.id += 1;
4446
id
4547
}
46-
}
4748

48-
impl TestClient for WsClient {
49-
async fn send<S: serde::Serialize>(&mut self, method: &str, params: &S) {
50-
let id = self.next_id();
51-
self.send_inner(&serde_json::json!({
52-
"jsonrpc": "2.0",
53-
"id": id,
54-
"method": method,
55-
"params": params,
56-
}))
57-
.await;
49+
async fn send_raw<S: serde::Serialize>(&mut self, msg: &S) {
50+
self.send_inner(msg).await;
5851
}
5952

6053
async fn recv<D: serde::de::DeserializeOwned>(&mut self) -> D {

0 commit comments

Comments
 (0)