use std::{net::SocketAddr, time::Duration};
use reqwest::Client;
use crate::BoxError;
const RPC_REQUEST_TIMEOUT: Duration = Duration::from_secs(180);
#[derive(Clone, Debug)]
pub struct RpcRequestClient {
client: Client,
rpc_address: SocketAddr,
}
impl RpcRequestClient {
pub fn new(rpc_address: SocketAddr) -> Self {
Self::new_with_timeout(rpc_address, RPC_REQUEST_TIMEOUT)
}
pub fn new_with_timeout(rpc_address: SocketAddr, timeout: Duration) -> Self {
Self {
client: Client::builder()
.timeout(timeout)
.build()
.expect("reqwest::Client build should not fail when only setting timeout"),
rpc_address,
}
}
pub async fn call(
&self,
method: impl AsRef<str>,
params: impl AsRef<str>,
) -> reqwest::Result<reqwest::Response> {
let method = method.as_ref();
let params = params.as_ref();
self.client
.post(format!("http://{}", self.rpc_address))
.body(format!(
r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
))
.header("Content-Type", "application/json")
.send()
.await
}
pub async fn call_with_content_type(
&self,
method: impl AsRef<str>,
params: impl AsRef<str>,
content_type: String,
) -> reqwest::Result<reqwest::Response> {
let method = method.as_ref();
let params = params.as_ref();
self.client
.post(format!("http://{}", self.rpc_address))
.body(format!(
r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
))
.header("Content-Type", content_type)
.send()
.await
}
pub async fn call_with_no_content_type(
&self,
method: impl AsRef<str>,
params: impl AsRef<str>,
) -> reqwest::Result<reqwest::Response> {
let method = method.as_ref();
let params = params.as_ref();
self.client
.post(format!("http://{}", self.rpc_address))
.body(format!(
r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
))
.send()
.await
}
pub async fn text_from_call(
&self,
method: impl AsRef<str>,
params: impl AsRef<str>,
) -> reqwest::Result<String> {
self.call(method, params).await?.text().await
}
pub async fn json_result_from_call<T: serde::de::DeserializeOwned>(
&self,
method: impl AsRef<str>,
params: impl AsRef<str>,
) -> std::result::Result<T, BoxError> {
Self::json_result_from_response_text(&self.text_from_call(method, params).await?)
}
fn json_result_from_response_text<T: serde::de::DeserializeOwned>(
response_text: &str,
) -> std::result::Result<T, BoxError> {
let output: jsonrpsee_types::Response<serde_json::Value> =
serde_json::from_str(response_text)?;
match output.payload {
jsonrpsee_types::ResponsePayload::Success(success) => {
Ok(serde_json::from_value(success.into_owned())?)
}
jsonrpsee_types::ResponsePayload::Error(failure) => Err(failure.to_string().into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn rpc_client_timeout_on_unresponsive_server() {
let listener =
std::net::TcpListener::bind("127.0.0.1:0").expect("should bind to localhost");
let addr = listener.local_addr().expect("should have a local address");
let held_stream = Arc::new(Mutex::new(None));
let held_stream_clone = held_stream.clone();
let accept_thread = std::thread::spawn(move || {
let (stream, _peer_addr) = listener.accept().expect("should accept a connection");
*held_stream_clone.lock().expect("lock poisoned") = Some(stream);
});
let short_timeout = Duration::from_secs(2);
let client = RpcRequestClient::new_with_timeout(addr, short_timeout);
let result = tokio::time::timeout(
Duration::from_secs(30),
client.text_from_call("getinfo", "[]"),
)
.await;
let inner_result =
result.expect("outer safety timeout should not fire; client timeout should fire first");
let err =
inner_result.expect_err("request to unresponsive server should fail with timeout");
assert!(err.is_timeout(), "error should be a timeout, got: {err}");
drop(held_stream);
accept_thread
.join()
.expect("accept thread should exit cleanly");
}
}