Skip to content

Commit 6e4d6f5

Browse files
committed
crc: RpcError expose punctual error types, #6250
1 parent 2834f8c commit 6e4d6f5

File tree

4 files changed

+102
-79
lines changed

4 files changed

+102
-79
lines changed

stacks-node/src/burnchains/rpc/bitcoin_rpc_client/tests.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ fn test_get_blockchain_info_fails_for_unknown_network() {
176176

177177
assert!(matches!(
178178
error,
179-
BitcoinRpcClientError::Rpc(RpcError::Decode(_))
179+
BitcoinRpcClientError::Rpc(RpcError::DecodeJson(_))
180180
));
181181
}
182182

@@ -433,7 +433,7 @@ fn test_generate_to_address_fails_for_invalid_block_hash() {
433433
.expect_err("Should fail!");
434434
assert!(matches!(
435435
error,
436-
BitcoinRpcClientError::Rpc(RpcError::Decode(_))
436+
BitcoinRpcClientError::Rpc(RpcError::DecodeJson(_))
437437
));
438438
}
439439

@@ -586,7 +586,7 @@ fn test_generate_block_fails_for_invalid_block_hash() {
586586
.expect_err("Should fail!");
587587
assert!(matches!(
588588
error,
589-
BitcoinRpcClientError::Rpc(RpcError::Decode(_))
589+
BitcoinRpcClientError::Rpc(RpcError::DecodeJson(_))
590590
));
591591
}
592592

@@ -842,7 +842,7 @@ fn test_get_new_address_fails_for_invalid_address() {
842842
.expect_err("Should fail!");
843843
assert!(matches!(
844844
error,
845-
BitcoinRpcClientError::Rpc(RpcError::Decode(_))
845+
BitcoinRpcClientError::Rpc(RpcError::DecodeJson(_))
846846
))
847847
}
848848

@@ -919,7 +919,7 @@ fn test_send_to_address_fails_for_invalid_tx_id() {
919919
.expect_err("Should fail!");
920920
assert!(matches!(
921921
error,
922-
BitcoinRpcClientError::Rpc(RpcError::Decode(_))
922+
BitcoinRpcClientError::Rpc(RpcError::DecodeJson(_))
923923
));
924924
}
925925

stacks-node/src/burnchains/rpc/rpc_transport/mod.rs

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -58,43 +58,62 @@ struct JsonRpcResponse<T> {
5858
/// Result returned from the RPC method, if successful.
5959
result: Option<T>,
6060
/// Error object returned by the RPC server, if the call failed.
61-
error: Option<Value>,
61+
error: Option<JsonRpcError>,
62+
}
63+
64+
/// Represents the JSON-RPC response error received from the endpoint
65+
#[derive(Deserialize, Debug, thiserror::Error)]
66+
#[error("JsonRpcError code {code}: {message}")]
67+
pub struct JsonRpcError {
68+
/// error code
69+
code: i32,
70+
/// human-readable error message
71+
message: String,
72+
/// data can be any JSON value or omitted
73+
data: Option<Value>,
6274
}
6375

6476
/// Represents a JSON-RPC error encountered during a transport operation.
65-
#[derive(Debug, Clone, Deserialize, Serialize)]
77+
#[derive(Debug, thiserror::Error)]
6678
pub enum RpcError {
67-
/// Represents a network-level error, such as connection failures or timeouts.
68-
Network(String),
69-
/// Indicates that the request could not be encoded properly
70-
Encode(String),
71-
/// Indicates that the response could not be decoded properly.
72-
Decode(String),
79+
// Serde decoding error
80+
#[error("JSON decoding error: {0}")]
81+
DecodeJson(serde_json::Error),
82+
// Serde encoding error
83+
#[error("JSON encoding error: {0}")]
84+
EncodeJson(serde_json::Error),
85+
/// Indicates that the response doesn't contain a json payload
86+
#[error("Invalid JSON payload error")]
87+
InvalidJsonPayload,
88+
// RPC Id mismatch between request and response
89+
#[error("Id Mismatch! Request: {0}, Response: {1}")]
90+
MismatchedId(String, String),
91+
// Stacks common network error
92+
#[error("Stacks Net error: {0}")]
93+
NetworkStacksCommon(#[from] stacks_common::types::net::Error),
94+
// Stacks network lib error
95+
#[error("IO error: {0}")]
96+
NetworkIO(#[from] io::Error),
97+
// Stacks lib network error
98+
#[error("Stacks Net error: {0}")]
99+
NetworkStacksLib(#[from] stacks::net::Error),
73100
/// Represents an error returned by the RPC service itself.
74-
Service(String),
101+
#[error("Service JSON error: {0}")]
102+
Service(JsonRpcError),
103+
// URL missing host error
104+
#[error("URL missing host error: {0}")]
105+
UrlMissingHost(Url),
106+
// URL missing port error
107+
#[error("URL missing port error: {0}")]
108+
UrlMissingPort(Url),
109+
// URL parse error
110+
#[error("URL error: {0}")]
111+
UrlParse(#[from] url::ParseError),
75112
}
76113

77114
/// Alias for results returned from RPC operations using `RpcTransport`.
78115
pub type RpcResult<T> = Result<T, RpcError>;
79116

80-
impl From<url::ParseError> for RpcError {
81-
fn from(e: url::ParseError) -> Self {
82-
Self::Network(format!("Url Error: {e:?}"))
83-
}
84-
}
85-
86-
impl From<stacks_common::types::net::Error> for RpcError {
87-
fn from(e: stacks_common::types::net::Error) -> Self {
88-
Self::Network(format!("Net Error: {e:?}"))
89-
}
90-
}
91-
92-
impl From<io::Error> for RpcError {
93-
fn from(e: io::Error) -> Self {
94-
Self::Network(format!("IO Error: {e:?}"))
95-
}
96-
}
97-
98117
/// Represents supported authentication mechanisms for RPC requests.
99118
#[derive(Debug, Clone)]
100119
pub enum RpcAuth {
@@ -136,10 +155,10 @@ impl RpcTransport {
136155
let url_obj = Url::parse(&url)?;
137156
let host = url_obj
138157
.host_str()
139-
.ok_or(RpcError::Network(format!("Missing host in url: {url}")))?;
158+
.ok_or(RpcError::UrlMissingHost(url_obj.clone()))?;
140159
let port = url_obj
141160
.port_or_known_default()
142-
.ok_or(RpcError::Network(format!("Missing port in url: {url}")))?;
161+
.ok_or(RpcError::UrlMissingHost(url_obj.clone()))?;
143162

144163
let peer: PeerHost = format!("{host}:{port}").parse()?;
145164
let path = url_obj.path().to_string();
@@ -177,20 +196,15 @@ impl RpcTransport {
177196
params: Value::Array(params),
178197
};
179198

180-
let json_payload = serde_json::to_value(payload)
181-
.map_err(|e| RpcError::Encode(format!("Failed to encode request as JSON: {e:?}")))?;
199+
let json_payload = serde_json::to_value(payload).map_err(RpcError::EncodeJson)?;
182200

183201
let mut request = StacksHttpRequest::new_for_peer(
184202
self.peer.clone(),
185203
"POST".to_string(),
186204
self.path.clone(),
187205
HttpRequestContents::new().payload_json(json_payload),
188-
)
189-
.map_err(|e| {
190-
RpcError::Encode(format!(
191-
"Failed to encode infallible data as HTTP request {e:?}"
192-
))
193-
})?;
206+
)?;
207+
194208
request.add_header("Connection".into(), "close".into());
195209

196210
if let Some(auth_header) = self.auth_header() {
@@ -203,27 +217,24 @@ impl RpcTransport {
203217
let response = send_http_request(&host, port, request, self.timeout)?;
204218
let json_response = match response.destruct().1 {
205219
HttpResponsePayload::JSON(js) => Ok(js),
206-
_ => Err(RpcError::Decode("Did not get a JSON response".to_string())),
220+
_ => Err(RpcError::InvalidJsonPayload),
207221
}?;
208222

209-
let parsed_response: JsonRpcResponse<T> = serde_json::from_value(json_response)
210-
.map_err(|e| RpcError::Decode(format!("Json Parse Error: {e:?}")))?;
223+
let parsed_response: JsonRpcResponse<T> =
224+
serde_json::from_value(json_response).map_err(RpcError::DecodeJson)?;
211225

212226
if id != parsed_response.id {
213-
return Err(RpcError::Decode(format!(
214-
"Invalid response: mismatched 'id': expected '{}', got '{}'",
215-
id, parsed_response.id
216-
)));
227+
return Err(RpcError::MismatchedId(id.to_string(), parsed_response.id));
217228
}
218229

219230
if let Some(error) = parsed_response.error {
220-
return Err(RpcError::Service(format!("{:#}", error)));
231+
return Err(RpcError::Service(error));
221232
}
222233

223234
if let Some(result) = parsed_response.result {
224235
Ok(result)
225236
} else {
226-
Ok(serde_json::from_value(Value::Null).unwrap())
237+
Ok(serde_json::from_value(Value::Null).map_err(RpcError::DecodeJson)?)
227238
}
228239
}
229240

stacks-node/src/burnchains/rpc/rpc_transport/tests.rs

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,19 @@ fn test_send_with_string_result_with_basic_auth_ok() {
110110
}
111111

112112
#[test]
113-
fn test_send_fails_with_network_error() {
114-
let transport = RpcTransport::new("http://127.0.0.1:65535".to_string(), RpcAuth::None, None)
113+
fn test_send_fails_due_to_unreachable_endpoint() {
114+
let unreachable_endpoint = "http://127.0.0.1:65535".to_string();
115+
let transport = RpcTransport::new(unreachable_endpoint, RpcAuth::None, None)
115116
.expect("Should be created properly!");
116117

117118
let result: RpcResult<Value> = transport.send("client_id", "dummy_method", vec![]);
118119
assert!(result.is_err());
119-
assert!(matches!(result.unwrap_err(), RpcError::Network(_)));
120+
121+
let err = result.unwrap_err();
122+
assert!(
123+
matches!(err, RpcError::NetworkIO(_)),
124+
"Expected NetworkIO error, got: {err:?}"
125+
);
120126
}
121127

122128
#[test]
@@ -133,10 +139,11 @@ fn test_send_fails_with_http_500() {
133139

134140
assert!(result.is_err());
135141
match result {
136-
Err(RpcError::Network(msg)) => {
137-
assert!(msg.contains("500"))
142+
Err(RpcError::NetworkIO(e)) => {
143+
let msg = e.to_string();
144+
assert!(msg.contains("500"), "Should contain error 500!");
138145
}
139-
_ => panic!("Expected error 500"),
146+
other => panic!("Expected NetworkIO error, got: {other:?}"),
140147
}
141148
}
142149

@@ -155,10 +162,14 @@ fn test_send_fails_with_invalid_json() {
155162

156163
assert!(result.is_err());
157164
match result {
158-
Err(RpcError::Network(msg)) => {
159-
assert!(msg.contains("invalid message"))
165+
Err(RpcError::NetworkIO(e)) => {
166+
let msg = e.to_string();
167+
assert!(
168+
msg.contains("invalid message"),
169+
"Should contain 'invalid message'!"
170+
)
160171
}
161-
_ => panic!("Expected network error"),
172+
other => panic!("Expected NetworkIO error, got: {other:?}"),
162173
}
163174
}
164175

@@ -184,7 +195,7 @@ fn test_send_ok_if_missing_both_result_and_error() {
184195
#[test]
185196
fn test_send_fails_with_invalid_id() {
186197
let response_body = json!({
187-
"id": "wrong_client_id",
198+
"id": "res_client_id_wrong",
188199
"result": true,
189200
});
190201

@@ -197,14 +208,14 @@ fn test_send_fails_with_invalid_id() {
197208
.create();
198209

199210
let transport = utils::rpc_no_auth(&server);
200-
let result: RpcResult<Value> = transport.send("client_id", "dummy", vec![]);
211+
let result: RpcResult<Value> = transport.send("req_client_id", "dummy", vec![]);
201212

202213
match result {
203-
Err(RpcError::Decode(msg)) => assert_eq!(
204-
"Invalid response: mismatched 'id': expected 'client_id', got 'wrong_client_id'",
205-
msg
206-
),
207-
_ => panic!("Expected missing result/error error"),
214+
Err(RpcError::MismatchedId(req_id, res_id)) => {
215+
assert_eq!("req_client_id", req_id);
216+
assert_eq!("res_client_id_wrong", res_id);
217+
}
218+
other => panic!("Expected MismatchedId, got {other:?}"),
208219
}
209220
}
210221

@@ -231,11 +242,11 @@ fn test_send_fails_with_service_error() {
231242
let result: RpcResult<Value> = transport.send("client_id", "unknown_method", vec![]);
232243

233244
match result {
234-
Err(RpcError::Service(msg)) => assert_eq!(
235-
"{\n \"code\": -32601,\n \"message\": \"Method not found\"\n}",
236-
msg
237-
),
238-
_ => panic!("Expected service error"),
245+
Err(RpcError::Service(err)) => {
246+
assert_eq!(-32601, err.code);
247+
assert_eq!("Method not found", err.message);
248+
}
249+
other => panic!("Expected Service error, got {other:?}"),
239250
}
240251
}
241252

@@ -275,9 +286,10 @@ fn test_send_fails_due_to_timeout() {
275286

276287
assert!(result.is_err());
277288
match result.unwrap_err() {
278-
RpcError::Network(msg) => {
279-
assert!(msg.contains("Timed out"));
289+
RpcError::NetworkIO(e) => {
290+
let msg = e.to_string();
291+
assert!(msg.contains("Timed out"), "Should contain 'Timed out'!");
280292
}
281-
err => panic!("Expected network error, got: {:?}", err),
293+
other => panic!("Expected NetworkIO error, got: {other:?}"),
282294
}
283295
}

stacks-node/src/tests/bitcoin_rpc_integrations.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn test_rpc_call_fails_when_bitcond_with_auth_but_rpc_no_auth() {
9797
let err = client.get_blockchain_info().expect_err("Should fail!");
9898

9999
assert!(
100-
matches!(err, BitcoinRpcClientError::Rpc(RpcError::Network(_))),
100+
matches!(err, BitcoinRpcClientError::Rpc(RpcError::NetworkIO(_))),
101101
"Expected RpcError::Network, got: {err:?}"
102102
);
103103
}
@@ -123,7 +123,7 @@ fn test_rpc_call_fails_when_bitcond_no_auth_and_rpc_no_auth() {
123123
let err = client.get_blockchain_info().expect_err("Should fail!");
124124

125125
assert!(
126-
matches!(err, BitcoinRpcClientError::Rpc(RpcError::Network(_))),
126+
matches!(err, BitcoinRpcClientError::Rpc(RpcError::NetworkIO(_))),
127127
"Expected RpcError::Network, got: {err:?}"
128128
);
129129
}
@@ -232,8 +232,8 @@ fn test_wallet_creation_fails_if_already_exists() {
232232
.expect_err("mywallet1 creation should fail now!");
233233

234234
match &err {
235-
BitcoinRpcClientError::Rpc(RpcError::Network(msg)) => {
236-
assert!(msg.contains("500"), "Bitcoind v25 returns HTTP 500)");
235+
BitcoinRpcClientError::Rpc(RpcError::NetworkIO(_)) => {
236+
assert!(true, "Bitcoind v25 returns HTTP 500)");
237237
}
238238
BitcoinRpcClientError::Rpc(RpcError::Service(_)) => {
239239
assert!(true, "Bitcoind v26+ returns HTTP 200");

0 commit comments

Comments
 (0)