1use alloy_json_rpc::{ErrorPayload, Id, RpcError, RpcResult};
2use serde::Deserialize;
3use serde_json::value::RawValue;
4use std::{error::Error as StdError, fmt::Debug, time::Duration};
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("{error}")]
45 HttpErrorWithRetryAfter {
46 #[source]
48 error: HttpError,
49 retry_after: Duration,
51 },
52
53 #[error("{0}")]
55 Custom(#[source] Box<dyn StdError + Send + Sync + 'static>),
56
57 #[error("{0}")]
63 NonRetryable(#[source] Box<dyn StdError + Send + Sync + 'static>),
64}
65
66impl TransportErrorKind {
67 pub const fn recoverable(&self) -> bool {
70 matches!(self, Self::MissingBatchResponse(_))
71 }
72
73 pub fn custom_str(err: &str) -> TransportError {
75 RpcError::Transport(Self::Custom(err.into()))
76 }
77
78 pub fn custom(err: impl StdError + Send + Sync + 'static) -> TransportError {
80 RpcError::Transport(Self::Custom(Box::new(err)))
81 }
82
83 pub fn non_retryable_str(err: &str) -> TransportError {
85 RpcError::Transport(Self::NonRetryable(err.into()))
86 }
87
88 pub fn non_retryable(err: impl StdError + Send + Sync + 'static) -> TransportError {
90 RpcError::Transport(Self::NonRetryable(Box::new(err)))
91 }
92
93 pub const fn is_non_retryable(&self) -> bool {
95 matches!(self, Self::NonRetryable(_))
96 }
97
98 pub const fn missing_batch_response(id: Id) -> TransportError {
100 RpcError::Transport(Self::MissingBatchResponse(id))
101 }
102
103 pub const fn backend_gone() -> TransportError {
105 RpcError::Transport(Self::BackendGone)
106 }
107
108 pub const fn pubsub_unavailable() -> TransportError {
110 RpcError::Transport(Self::PubsubUnavailable)
111 }
112
113 pub const fn http_error(status: u16, body: String) -> TransportError {
115 RpcError::Transport(Self::HttpError(HttpError { status, body }))
116 }
117
118 pub const fn http_error_with_retry_after(
121 status: u16,
122 body: String,
123 retry_after: Option<Duration>,
124 ) -> TransportError {
125 match retry_after {
126 Some(retry_after) => RpcError::Transport(Self::HttpErrorWithRetryAfter {
127 error: HttpError { status, body },
128 retry_after,
129 }),
130 None => Self::http_error(status, body),
131 }
132 }
133
134 pub const fn is_pubsub_unavailable(&self) -> bool {
136 matches!(self, Self::PubsubUnavailable)
137 }
138
139 pub const fn is_backend_gone(&self) -> bool {
141 matches!(self, Self::BackendGone)
142 }
143
144 pub const fn is_http_error(&self) -> bool {
146 matches!(self, Self::HttpError(_) | Self::HttpErrorWithRetryAfter { .. })
147 }
148
149 pub const fn as_http_error(&self) -> Option<&HttpError> {
151 match self {
152 Self::HttpError(err) => Some(err),
153 Self::HttpErrorWithRetryAfter { error, .. } => Some(error),
154 _ => None,
155 }
156 }
157
158 pub const fn retry_after(&self) -> Option<Duration> {
160 match self {
161 Self::HttpErrorWithRetryAfter { retry_after, .. } => Some(*retry_after),
162 _ => None,
163 }
164 }
165
166 pub const fn as_custom(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
168 match self {
169 Self::Custom(err) => Some(&**err),
170 _ => None,
171 }
172 }
173
174 pub fn is_retry_err(&self) -> bool {
177 match self {
178 Self::MissingBatchResponse(_) => true,
180 Self::HttpError(http_err) => {
181 http_err.is_rate_limit_err() || http_err.is_temporarily_unavailable()
182 }
183 Self::HttpErrorWithRetryAfter { error, .. } => error.status >= 400,
186 Self::Custom(err) => {
187 let msg = err.to_string();
188 msg.contains("429 Too Many Requests")
189 }
190 _ => false,
191 }
192 }
193}
194
195#[derive(Debug, thiserror::Error)]
197#[error(
198 "HTTP error {status} with {}",
199 if body.is_empty() { "empty body".to_string() } else { format!("body: {body}") }
200)]
201pub struct HttpError {
202 pub status: u16,
204 pub body: String,
206}
207
208impl HttpError {
209 pub const fn is_rate_limit_err(&self) -> bool {
211 self.status == 429
212 }
213
214 pub const fn is_temporarily_unavailable(&self) -> bool {
217 self.status == 503
218 }
219}
220
221pub(crate) trait RpcErrorExt {
223 fn is_retryable(&self) -> bool;
225
226 fn backoff_hint(&self) -> Option<std::time::Duration>;
228}
229
230impl RpcErrorExt for RpcError<TransportErrorKind> {
231 fn is_retryable(&self) -> bool {
232 match self {
233 Self::Transport(err) => err.is_retry_err(),
236 Self::SerError(_) => false,
239 Self::DeserError { text, .. } => {
240 if let Ok(resp) = serde_json::from_str::<ErrorPayload>(text) {
241 return resp.is_retry_err();
242 }
243
244 #[derive(Deserialize)]
247 struct Resp {
248 error: ErrorPayload,
249 }
250
251 if let Ok(resp) = serde_json::from_str::<Resp>(text) {
252 return resp.error.is_retry_err();
253 }
254
255 false
256 }
257 Self::ErrorResp(err) => err.is_retry_err(),
258 Self::NullResp => true,
259 _ => false,
260 }
261 }
262
263 fn backoff_hint(&self) -> Option<std::time::Duration> {
264 raw_backoff_hint(self).map(|hint| hint.min(MAX_BACKOFF_HINT))
267 }
268}
269
270const MAX_BACKOFF_HINT: Duration = Duration::from_secs(5 * 60);
273
274fn raw_backoff_hint(err: &RpcError<TransportErrorKind>) -> Option<Duration> {
276 if let RpcError::Transport(TransportErrorKind::HttpErrorWithRetryAfter {
277 retry_after, ..
278 }) = err
279 {
280 return Some(*retry_after);
281 }
282
283 if let RpcError::ErrorResp(resp) = err {
284 let data = resp.try_data_as::<serde_json::Value>();
286 if let Some(Ok(data)) = data {
287 let backoff_seconds = &data["rate"]["backoff_seconds"];
290 if let Some(seconds) = backoff_seconds.as_u64() {
292 return Some(Duration::from_secs(seconds));
293 }
294 if let Some(seconds) = backoff_seconds.as_f64() {
295 return Some(Duration::from_secs((seconds as u64).saturating_add(1)));
296 }
297 }
298
299 if let Some(duration) = parse_retry_after(&resp.message) {
301 return Some(duration);
302 }
303 }
304 None
305}
306
307fn parse_retry_after(message: &str) -> Option<std::time::Duration> {
309 let after = message.split_once("try again in ")?.1.trim_start();
310
311 let digits_len = after.as_bytes().iter().take_while(|b| b.is_ascii_digit()).count();
312 let (digits, rest) = after.split_at(digits_len);
313 let value: u64 = digits.parse().ok()?;
314
315 let unit = rest.trim().trim_end_matches(|c: char| c.is_ascii_punctuation());
316 match unit {
317 "ms" => Some(std::time::Duration::from_millis(value)),
318 "s" => Some(std::time::Duration::from_secs(value)),
319 _ => None,
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_retry_error() {
329 let err = "{\"code\":-32007,\"message\":\"100/second request limit reached - reduce calls per second or upgrade your account at quicknode.com\"}";
330 let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
331 assert!(TransportError::ErrorResp(err).is_retryable());
332 }
333
334 #[test]
335 fn test_retry_error_rate_limited() {
336 let err = r#"{"code":-32005,"message":"rate limited, try again in 4ms","data":null}"#;
337 let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
338 let err = TransportError::ErrorResp(err);
339 assert!(err.is_retryable());
340 assert_eq!(err.backoff_hint(), Some(std::time::Duration::from_millis(4)));
341 }
342
343 #[test]
344 fn parse_retry_after_millis() {
345 assert_eq!(
346 parse_retry_after("try again in 4ms"),
347 Some(std::time::Duration::from_millis(4))
348 );
349 assert_eq!(
350 parse_retry_after("rate limited, try again in 100ms"),
351 Some(std::time::Duration::from_millis(100))
352 );
353 }
354
355 #[test]
356 fn parse_retry_after_seconds() {
357 assert_eq!(parse_retry_after("try again in 2s"), Some(std::time::Duration::from_secs(2)));
358 }
359
360 #[test]
361 fn parse_retry_after_none() {
362 assert_eq!(parse_retry_after("some other error"), None);
363 assert_eq!(parse_retry_after("try again in"), None);
364 assert_eq!(parse_retry_after("try again in ms"), None);
365 assert_eq!(parse_retry_after("try again in 4us"), None);
366 }
367
368 #[test]
369 fn test_retry_error_429() {
370 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."}"#;
371 let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
372 assert!(TransportError::ErrorResp(err).is_retryable());
373 }
374
375 #[test]
376 fn http_retry_after_retryable_for_error_statuses() {
377 let err = TransportErrorKind::http_error_with_retry_after(
378 500,
379 String::new(),
380 Some(Duration::from_secs(1)),
381 );
382 assert!(err.is_retryable());
383 assert_eq!(err.backoff_hint(), Some(Duration::from_secs(1)));
384
385 assert!(!TransportErrorKind::http_error(500, String::new()).is_retryable());
387
388 let err = TransportErrorKind::http_error_with_retry_after(
390 301,
391 String::new(),
392 Some(Duration::from_secs(1)),
393 );
394 assert!(!err.is_retryable());
395 }
396
397 #[test]
398 fn backoff_hint_clamped() {
399 let err = TransportErrorKind::http_error_with_retry_after(
400 429,
401 String::new(),
402 Some(Duration::from_secs(u64::MAX)),
403 );
404 assert_eq!(err.backoff_hint(), Some(MAX_BACKOFF_HINT));
405 }
406
407 #[test]
408 fn http_retry_after_preserves_http_error() {
409 let err = TransportErrorKind::http_error_with_retry_after(
410 429,
411 "Too Many Requests".to_owned(),
412 Some(Duration::from_secs(52)),
413 );
414
415 assert!(err.is_retryable());
416 assert_eq!(err.backoff_hint(), Some(Duration::from_secs(52)));
417
418 let TransportError::Transport(kind) = err else { panic!("expected transport error") };
419 assert!(kind.is_http_error());
420 assert_eq!(kind.retry_after(), Some(Duration::from_secs(52)));
421 assert_eq!(kind.as_http_error().unwrap().status, 429);
422 }
423}