1use alloy_json_rpc::{ErrorPayload, Id, RpcError, RpcResult};
2use serde::Deserialize;
3use serde_json::value::RawValue;
4use std::{error::Error as StdError, fmt::Debug};
5use thiserror::Error;
6
7pub type TransportError<ErrResp = Box<RawValue>> = RpcError<TransportErrorKind, ErrResp>;
9
10pub type TransportResult<T, ErrResp = Box<RawValue>> = RpcResult<T, TransportErrorKind, ErrResp>;
12
13#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum TransportErrorKind {
19 #[error("missing response for request with ID {0}")]
25 MissingBatchResponse(Id),
26
27 #[error("backend connection task has stopped")]
29 BackendGone,
30
31 #[error("subscriptions are not available on this provider")]
33 PubsubUnavailable,
34
35 #[error("{0}")]
37 HttpError(#[from] HttpError),
38
39 #[error("{0}")]
41 Custom(#[source] Box<dyn StdError + Send + Sync + 'static>),
42
43 #[error("{0}")]
49 NonRetryable(#[source] Box<dyn StdError + Send + Sync + 'static>),
50}
51
52impl TransportErrorKind {
53 pub const fn recoverable(&self) -> bool {
56 matches!(self, Self::MissingBatchResponse(_))
57 }
58
59 pub fn custom_str(err: &str) -> TransportError {
61 RpcError::Transport(Self::Custom(err.into()))
62 }
63
64 pub fn custom(err: impl StdError + Send + Sync + 'static) -> TransportError {
66 RpcError::Transport(Self::Custom(Box::new(err)))
67 }
68
69 pub fn non_retryable_str(err: &str) -> TransportError {
71 RpcError::Transport(Self::NonRetryable(err.into()))
72 }
73
74 pub fn non_retryable(err: impl StdError + Send + Sync + 'static) -> TransportError {
76 RpcError::Transport(Self::NonRetryable(Box::new(err)))
77 }
78
79 pub const fn is_non_retryable(&self) -> bool {
81 matches!(self, Self::NonRetryable(_))
82 }
83
84 pub const fn missing_batch_response(id: Id) -> TransportError {
86 RpcError::Transport(Self::MissingBatchResponse(id))
87 }
88
89 pub const fn backend_gone() -> TransportError {
91 RpcError::Transport(Self::BackendGone)
92 }
93
94 pub const fn pubsub_unavailable() -> TransportError {
96 RpcError::Transport(Self::PubsubUnavailable)
97 }
98
99 pub const fn http_error(status: u16, body: String) -> TransportError {
101 RpcError::Transport(Self::HttpError(HttpError { status, body }))
102 }
103
104 pub const fn is_pubsub_unavailable(&self) -> bool {
106 matches!(self, Self::PubsubUnavailable)
107 }
108
109 pub const fn is_backend_gone(&self) -> bool {
111 matches!(self, Self::BackendGone)
112 }
113
114 pub const fn is_http_error(&self) -> bool {
116 matches!(self, Self::HttpError(_))
117 }
118
119 pub const fn as_http_error(&self) -> Option<&HttpError> {
121 match self {
122 Self::HttpError(err) => Some(err),
123 _ => None,
124 }
125 }
126
127 pub const fn as_custom(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
129 match self {
130 Self::Custom(err) => Some(&**err),
131 _ => None,
132 }
133 }
134
135 pub fn is_retry_err(&self) -> bool {
138 match self {
139 Self::MissingBatchResponse(_) => true,
141 Self::HttpError(http_err) => {
142 http_err.is_rate_limit_err() || http_err.is_temporarily_unavailable()
143 }
144 Self::Custom(err) => {
145 let msg = err.to_string();
146 msg.contains("429 Too Many Requests")
147 }
148 _ => false,
149 }
150 }
151}
152
153#[derive(Debug, thiserror::Error)]
155#[error(
156 "HTTP error {status} with {}",
157 if body.is_empty() { "empty body".to_string() } else { format!("body: {body}") }
158)]
159pub struct HttpError {
160 pub status: u16,
162 pub body: String,
164}
165
166impl HttpError {
167 pub const fn is_rate_limit_err(&self) -> bool {
169 self.status == 429
170 }
171
172 pub const fn is_temporarily_unavailable(&self) -> bool {
175 self.status == 503
176 }
177}
178
179pub(crate) trait RpcErrorExt {
181 fn is_retryable(&self) -> bool;
183
184 fn backoff_hint(&self) -> Option<std::time::Duration>;
186}
187
188impl RpcErrorExt for RpcError<TransportErrorKind> {
189 fn is_retryable(&self) -> bool {
190 match self {
191 Self::Transport(err) => err.is_retry_err(),
194 Self::SerError(_) => false,
197 Self::DeserError { text, .. } => {
198 if let Ok(resp) = serde_json::from_str::<ErrorPayload>(text) {
199 return resp.is_retry_err();
200 }
201
202 #[derive(Deserialize)]
205 struct Resp {
206 error: ErrorPayload,
207 }
208
209 if let Ok(resp) = serde_json::from_str::<Resp>(text) {
210 return resp.error.is_retry_err();
211 }
212
213 false
214 }
215 Self::ErrorResp(err) => err.is_retry_err(),
216 Self::NullResp => true,
217 _ => false,
218 }
219 }
220
221 fn backoff_hint(&self) -> Option<std::time::Duration> {
222 if let Self::ErrorResp(resp) = self {
223 let data = resp.try_data_as::<serde_json::Value>();
225 if let Some(Ok(data)) = data {
226 let backoff_seconds = &data["rate"]["backoff_seconds"];
229 if let Some(seconds) = backoff_seconds.as_u64() {
231 return Some(std::time::Duration::from_secs(seconds));
232 }
233 if let Some(seconds) = backoff_seconds.as_f64() {
234 return Some(std::time::Duration::from_secs(seconds as u64 + 1));
235 }
236 }
237
238 if let Some(duration) = parse_retry_after(&resp.message) {
240 return Some(duration);
241 }
242 }
243 None
244 }
245}
246
247fn parse_retry_after(message: &str) -> Option<std::time::Duration> {
249 let after = message.split_once("try again in ")?.1.trim_start();
250
251 let digits_len = after.as_bytes().iter().take_while(|b| b.is_ascii_digit()).count();
252 let (digits, rest) = after.split_at(digits_len);
253 let value: u64 = digits.parse().ok()?;
254
255 let unit = rest.trim().trim_end_matches(|c: char| c.is_ascii_punctuation());
256 match unit {
257 "ms" => Some(std::time::Duration::from_millis(value)),
258 "s" => Some(std::time::Duration::from_secs(value)),
259 _ => None,
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_retry_error() {
269 let err = "{\"code\":-32007,\"message\":\"100/second request limit reached - reduce calls per second or upgrade your account at quicknode.com\"}";
270 let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
271 assert!(TransportError::ErrorResp(err).is_retryable());
272 }
273
274 #[test]
275 fn test_retry_error_rate_limited() {
276 let err = r#"{"code":-32005,"message":"rate limited, try again in 4ms","data":null}"#;
277 let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
278 let err = TransportError::ErrorResp(err);
279 assert!(err.is_retryable());
280 assert_eq!(err.backoff_hint(), Some(std::time::Duration::from_millis(4)));
281 }
282
283 #[test]
284 fn parse_retry_after_millis() {
285 assert_eq!(
286 parse_retry_after("try again in 4ms"),
287 Some(std::time::Duration::from_millis(4))
288 );
289 assert_eq!(
290 parse_retry_after("rate limited, try again in 100ms"),
291 Some(std::time::Duration::from_millis(100))
292 );
293 }
294
295 #[test]
296 fn parse_retry_after_seconds() {
297 assert_eq!(parse_retry_after("try again in 2s"), Some(std::time::Duration::from_secs(2)));
298 }
299
300 #[test]
301 fn parse_retry_after_none() {
302 assert_eq!(parse_retry_after("some other error"), None);
303 assert_eq!(parse_retry_after("try again in"), None);
304 assert_eq!(parse_retry_after("try again in ms"), None);
305 assert_eq!(parse_retry_after("try again in 4us"), None);
306 }
307
308 #[test]
309 fn test_retry_error_429() {
310 let err = r#"{"code":429,"event":-33200,"message":"Too Many Requests","details":"You have surpassed your allowed throughput limit. Reduce the amount of requests per second or upgrade for more capacity."}"#;
311 let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
312 assert!(TransportError::ErrorResp(err).is_retryable());
313 }
314}