use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
use serde::de::DeserializeOwned;
use serde::Deserialize;
use std::time::Duration;
use thiserror::Error;
fn truncate_chars(s: &str, max: usize) -> String {
let t: String = s.chars().take(max).collect();
if s.chars().count() <= max {
s.to_owned()
} else {
t
}
}
#[derive(Debug, Deserialize)]
pub struct RpcError {
pub code: i32,
pub message: String,
}
#[derive(Debug, Error)]
pub enum RpcCallError {
#[error("transport error: {0}")]
Transport(#[from] reqwest::Error),
#[error("error encoding request body for {method}: {error}")]
Encode { error: String, method: String },
#[error("HTTP {status} calling {method}: body: {body_snippet}")]
Http {
status: reqwest::StatusCode,
method: String,
body_snippet: String,
},
#[error("{method} error {code}: {message}")]
Rpc {
code: i32,
message: String,
method: String,
},
#[error("error decoding response body for {method}: {error}; body: {body_snippet}")]
Decode {
error: String,
method: String,
body_snippet: String,
},
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum RpcOutcome<T> {
Ok { result: T },
Err { error: RpcError },
}
pub async fn rpc_call<T: DeserializeOwned>(
client: &reqwest::Client,
url: &str,
method: &str,
params: serde_json::Value,
timeout: Duration,
) -> Result<T, RpcCallError> {
let req = serde_json::json!({
"id": 1,
"jsonrpc": "2.0",
"method": method,
"params": params,
});
let body = serde_json::to_vec(&req).map_err(|e| RpcCallError::Encode {
error: e.to_string(),
method: method.to_string(),
})?;
if std::env::var("DEBUG_I2PCONTROL_REQ").ok().as_deref() == Some("1") {
if let Ok(body_str) = serde_json::to_string(&req) {
log::info!("{} request body: {}", method, body_str);
}
}
let content_length = body.len() as u64;
let resp = client
.post(url)
.header(CONTENT_TYPE, "application/json")
.header(CONTENT_LENGTH, content_length)
.body(body)
.timeout(timeout)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
let body_snippet = if body.chars().count() > 2048 {
truncate_chars(&body, 2048)
} else {
body.clone()
};
return Err(RpcCallError::Http {
status,
method: method.to_string(),
body_snippet,
});
}
let text = resp.text().await?;
if std::env::var("DEBUG_I2PCONTROL_BODY").ok().as_deref() == Some("1") {
let snippet = if text.chars().count() > 4096 {
truncate_chars(&text, 4096)
} else {
text.clone()
};
log::debug!("{} response body: {}", method, snippet);
}
let parsed: Result<RpcOutcome<T>, _> = serde_json::from_str(&text);
match parsed {
Ok(RpcOutcome::Ok { result }) => Ok(result),
Ok(RpcOutcome::Err { error }) => Err(RpcCallError::Rpc {
code: error.code,
message: error.message,
method: method.to_string(),
}),
Err(e) => {
let body_snippet = if text.chars().count() > 2048 {
truncate_chars(&text, 2048)
} else {
text.clone()
};
Err(RpcCallError::Decode {
error: e.to_string(),
method: method.to_string(),
body_snippet,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_chars() {
assert_eq!(truncate_chars("abcd", 10), "abcd");
assert_eq!(truncate_chars("abcdef", 4), "abcd");
assert_eq!(truncate_chars("éèà", 2), "éè");
}
}