use std::fmt::Display;
use alloy::transports::{RpcError as AlloyRpcError, TransportErrorKind};
use thiserror::Error;
fn redact_url_paths(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut pos = 0;
while pos < s.len() {
let Some(idx) = s[pos..].find("://") else {
result.push_str(&s[pos..]);
break;
};
let abs = pos + idx;
result.push_str(&s[pos..abs + 3]); pos = abs + 3;
let token_end = s[pos..]
.find(|c: char| c.is_whitespace() || matches!(c, '(' | ')' | '"' | '\''))
.map(|i| pos + i)
.unwrap_or(s.len());
if let Some(slash) = s[pos..token_end].find('/') {
result.push_str(&s[pos..pos + slash]); result.push_str("/***");
} else {
result.push_str(&s[pos..token_end]); }
pos = token_end;
}
result
}
#[derive(Error, Debug)]
pub struct ReqwestError {
pub msg: String,
#[source]
pub source: AlloyRpcError<TransportErrorKind>,
}
impl Display for ReqwestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.msg, redact_url_paths(&self.source.to_string()))
}
}
#[derive(Error, Debug)]
pub enum RequestError {
Reqwest(ReqwestError),
Other(String),
}
impl Display for RequestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RequestError::Reqwest(e) => write!(f, "{e}"),
RequestError::Other(e) => write!(f, "{e}"),
}
}
}
#[derive(Error, Debug)]
pub enum RPCError {
#[error("RPC setup error: {0}")]
SetupError(String),
#[error("Request error: {0}")]
RequestError(RequestError),
#[error("Tracing failure: {0}")]
TracingFailure(String),
#[error("Unknown error: {0}")]
UnknownError(String),
}
impl RPCError {
pub(super) fn from_alloy<S: ToString>(
msg: S,
error: AlloyRpcError<TransportErrorKind>,
) -> Self {
RPCError::RequestError(RequestError::Reqwest(ReqwestError {
msg: msg.to_string(),
source: error,
}))
}
}
#[cfg(test)]
mod tests {
use super::redact_url_paths;
#[test]
fn redacts_path_component() {
let input =
"error sending request for url (https://arbitrum.chainstack.com/supersecretkey)";
let output = redact_url_paths(input);
assert_eq!(output, "error sending request for url (https://arbitrum.chainstack.com/***)");
}
#[test]
fn preserves_url_without_path() {
let input = "error sending request for url (https://arbitrum.chainstack.com)";
let output = redact_url_paths(input);
assert_eq!(output, input);
}
#[test]
fn passthrough_when_no_url() {
let input = "connection refused";
assert_eq!(redact_url_paths(input), input);
}
#[test]
fn redacts_multiple_urls() {
let input = "primary https://a.example.com/key1 fallback https://b.example.com/key2";
let output = redact_url_paths(input);
assert_eq!(output, "primary https://a.example.com/*** fallback https://b.example.com/***");
}
#[test]
fn redacts_url_at_end_of_string() {
let input = "failed: https://node.example.com/apikey";
let output = redact_url_paths(input);
assert_eq!(output, "failed: https://node.example.com/***");
}
}