zebra_node_services/
rpc_client.rs1use std::{net::SocketAddr, time::Duration};
6
7use reqwest::Client;
8
9use crate::BoxError;
10
11const RPC_REQUEST_TIMEOUT: Duration = Duration::from_secs(180);
16
17#[derive(Clone, Debug)]
19pub struct RpcRequestClient {
20 client: Client,
21 rpc_address: SocketAddr,
22}
23
24impl RpcRequestClient {
25 pub fn new(rpc_address: SocketAddr) -> Self {
27 Self::new_with_timeout(rpc_address, RPC_REQUEST_TIMEOUT)
28 }
29
30 pub fn new_with_timeout(rpc_address: SocketAddr, timeout: Duration) -> Self {
34 Self {
35 client: Client::builder()
36 .timeout(timeout)
37 .build()
38 .expect("reqwest::Client build should not fail when only setting timeout"),
39 rpc_address,
40 }
41 }
42
43 pub async fn call(
45 &self,
46 method: impl AsRef<str>,
47 params: impl AsRef<str>,
48 ) -> reqwest::Result<reqwest::Response> {
49 let method = method.as_ref();
50 let params = params.as_ref();
51
52 self.client
53 .post(format!("http://{}", self.rpc_address))
54 .body(format!(
55 r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
56 ))
57 .header("Content-Type", "application/json")
58 .send()
59 .await
60 }
61
62 pub async fn call_with_content_type(
64 &self,
65 method: impl AsRef<str>,
66 params: impl AsRef<str>,
67 content_type: String,
68 ) -> reqwest::Result<reqwest::Response> {
69 let method = method.as_ref();
70 let params = params.as_ref();
71
72 self.client
73 .post(format!("http://{}", self.rpc_address))
74 .body(format!(
75 r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
76 ))
77 .header("Content-Type", content_type)
78 .send()
79 .await
80 }
81
82 pub async fn call_with_no_content_type(
84 &self,
85 method: impl AsRef<str>,
86 params: impl AsRef<str>,
87 ) -> reqwest::Result<reqwest::Response> {
88 let method = method.as_ref();
89 let params = params.as_ref();
90
91 self.client
92 .post(format!("http://{}", self.rpc_address))
93 .body(format!(
94 r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
95 ))
96 .send()
97 .await
98 }
99
100 pub async fn text_from_call(
102 &self,
103 method: impl AsRef<str>,
104 params: impl AsRef<str>,
105 ) -> reqwest::Result<String> {
106 self.call(method, params).await?.text().await
107 }
108
109 pub async fn json_result_from_call<T: serde::de::DeserializeOwned>(
115 &self,
116 method: impl AsRef<str>,
117 params: impl AsRef<str>,
118 ) -> std::result::Result<T, BoxError> {
119 Self::json_result_from_response_text(&self.text_from_call(method, params).await?)
120 }
121
122 fn json_result_from_response_text<T: serde::de::DeserializeOwned>(
125 response_text: &str,
126 ) -> std::result::Result<T, BoxError> {
127 let output: jsonrpsee_types::Response<serde_json::Value> =
128 serde_json::from_str(response_text)?;
129 match output.payload {
130 jsonrpsee_types::ResponsePayload::Success(success) => {
131 Ok(serde_json::from_value(success.into_owned())?)
132 }
133 jsonrpsee_types::ResponsePayload::Error(failure) => Err(failure.to_string().into()),
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::sync::{Arc, Mutex};
142
143 #[tokio::test]
146 async fn rpc_client_timeout_on_unresponsive_server() {
147 let listener =
148 std::net::TcpListener::bind("127.0.0.1:0").expect("should bind to localhost");
149 let addr = listener.local_addr().expect("should have a local address");
150
151 let held_stream = Arc::new(Mutex::new(None));
153 let held_stream_clone = held_stream.clone();
154
155 let accept_thread = std::thread::spawn(move || {
157 let (stream, _peer_addr) = listener.accept().expect("should accept a connection");
158 *held_stream_clone.lock().expect("lock poisoned") = Some(stream);
159 });
160
161 let short_timeout = Duration::from_secs(2);
162 let client = RpcRequestClient::new_with_timeout(addr, short_timeout);
163
164 let result = tokio::time::timeout(
166 Duration::from_secs(30),
167 client.text_from_call("getinfo", "[]"),
168 )
169 .await;
170
171 let inner_result =
172 result.expect("outer safety timeout should not fire; client timeout should fire first");
173
174 let err =
175 inner_result.expect_err("request to unresponsive server should fail with timeout");
176 assert!(err.is_timeout(), "error should be a timeout, got: {err}");
177
178 drop(held_stream);
179 accept_thread
180 .join()
181 .expect("accept thread should exit cleanly");
182 }
183}