alloy_transport/
error.rs

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
7/// A transport error is an [`RpcError`] containing a [`TransportErrorKind`].
8pub type TransportError<ErrResp = Box<RawValue>> = RpcError<TransportErrorKind, ErrResp>;
9
10/// A transport result is a [`Result`] containing a [`TransportError`].
11pub type TransportResult<T, ErrResp = Box<RawValue>> = RpcResult<T, TransportErrorKind, ErrResp>;
12
13/// Transport error.
14///
15/// All transport errors are wrapped in this enum.
16#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum TransportErrorKind {
19    /// Missing batch response.
20    ///
21    /// This error is returned when a batch request is sent and the response
22    /// does not contain a response for a request. For convenience the ID is
23    /// specified.
24    #[error("missing response for request with ID {0}")]
25    MissingBatchResponse(Id),
26
27    /// Backend connection task has stopped.
28    #[error("backend connection task has stopped")]
29    BackendGone,
30
31    /// Pubsub service is not available for the current provider.
32    #[error("subscriptions are not available on this provider")]
33    PubsubUnavailable,
34
35    /// HTTP Error with code and body
36    #[error("{0}")]
37    HttpError(#[from] HttpError),
38
39    /// Custom error.
40    #[error("{0}")]
41    Custom(#[source] Box<dyn StdError + Send + Sync + 'static>),
42}
43
44impl TransportErrorKind {
45    /// Returns `true` if the error is potentially recoverable.
46    /// This is a naive heuristic and should be used with caution.
47    pub const fn recoverable(&self) -> bool {
48        matches!(self, Self::MissingBatchResponse(_))
49    }
50
51    /// Instantiate a new `TransportError` from a custom error.
52    pub fn custom_str(err: &str) -> TransportError {
53        RpcError::Transport(Self::Custom(err.into()))
54    }
55
56    /// Instantiate a new `TransportError` from a custom error.
57    pub fn custom(err: impl StdError + Send + Sync + 'static) -> TransportError {
58        RpcError::Transport(Self::Custom(Box::new(err)))
59    }
60
61    /// Instantiate a new `TransportError` from a missing ID.
62    pub const fn missing_batch_response(id: Id) -> TransportError {
63        RpcError::Transport(Self::MissingBatchResponse(id))
64    }
65
66    /// Instantiate a new `TransportError::BackendGone`.
67    pub const fn backend_gone() -> TransportError {
68        RpcError::Transport(Self::BackendGone)
69    }
70
71    /// Instantiate a new `TransportError::PubsubUnavailable`.
72    pub const fn pubsub_unavailable() -> TransportError {
73        RpcError::Transport(Self::PubsubUnavailable)
74    }
75
76    /// Instantiate a new `TransportError::HttpError`.
77    pub const fn http_error(status: u16, body: String) -> TransportError {
78        RpcError::Transport(Self::HttpError(HttpError { status, body }))
79    }
80
81    /// Analyzes the [TransportErrorKind] and decides if the request should be retried based on the
82    /// variant.
83    pub fn is_retry_err(&self) -> bool {
84        match self {
85            // Missing batch response errors can be retried.
86            Self::MissingBatchResponse(_) => true,
87            Self::HttpError(http_err) => {
88                http_err.is_rate_limit_err() || http_err.is_temporarily_unavailable()
89            }
90            Self::Custom(err) => {
91                let msg = err.to_string();
92                msg.contains("429 Too Many Requests")
93            }
94            _ => false,
95        }
96    }
97}
98
99/// Type for holding HTTP errors such as 429 rate limit error.
100#[derive(Debug, thiserror::Error)]
101#[error(
102    "HTTP error {status} with {}",
103    if body.is_empty() { "empty body".to_string() } else { format!("body: {body}") }
104)]
105pub struct HttpError {
106    /// The HTTP status code.
107    pub status: u16,
108    /// The HTTP response body.
109    pub body: String,
110}
111
112impl HttpError {
113    /// Checks the `status` to determine whether the request should be retried.
114    pub const fn is_rate_limit_err(&self) -> bool {
115        self.status == 429
116    }
117
118    /// Checks the `status` to determine whether the service was temporarily unavailable and should
119    /// be retried.
120    pub const fn is_temporarily_unavailable(&self) -> bool {
121        self.status == 503
122    }
123}
124
125/// Extension trait to implement methods for [`RpcError<TransportErrorKind, E>`].
126pub(crate) trait RpcErrorExt {
127    /// Analyzes whether to retry the request depending on the error.
128    fn is_retryable(&self) -> bool;
129
130    /// Fetches the backoff hint from the error message if present
131    fn backoff_hint(&self) -> Option<std::time::Duration>;
132}
133
134impl RpcErrorExt for RpcError<TransportErrorKind> {
135    fn is_retryable(&self) -> bool {
136        match self {
137            // There was a transport-level error. This is either a non-retryable error,
138            // or a server error that should be retried.
139            Self::Transport(err) => err.is_retry_err(),
140            // The transport could not serialize the error itself. The request was malformed from
141            // the start.
142            Self::SerError(_) => false,
143            Self::DeserError { text, .. } => {
144                if let Ok(resp) = serde_json::from_str::<ErrorPayload>(text) {
145                    return resp.is_retry_err();
146                }
147
148                // some providers send invalid JSON RPC in the error case (no `id:u64`), but the
149                // text should be a `JsonRpcError`
150                #[derive(Deserialize)]
151                struct Resp {
152                    error: ErrorPayload,
153                }
154
155                if let Ok(resp) = serde_json::from_str::<Resp>(text) {
156                    return resp.error.is_retry_err();
157                }
158
159                false
160            }
161            Self::ErrorResp(err) => err.is_retry_err(),
162            Self::NullResp => true,
163            _ => false,
164        }
165    }
166
167    fn backoff_hint(&self) -> Option<std::time::Duration> {
168        if let Self::ErrorResp(resp) = self {
169            let data = resp.try_data_as::<serde_json::Value>();
170            if let Some(Ok(data)) = data {
171                // if daily rate limit exceeded, infura returns the requested backoff in the error
172                // response
173                let backoff_seconds = &data["rate"]["backoff_seconds"];
174                // infura rate limit error
175                if let Some(seconds) = backoff_seconds.as_u64() {
176                    return Some(std::time::Duration::from_secs(seconds));
177                }
178                if let Some(seconds) = backoff_seconds.as_f64() {
179                    return Some(std::time::Duration::from_secs(seconds as u64 + 1));
180                }
181            }
182        }
183        None
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_retry_error() {
193        let err = "{\"code\":-32007,\"message\":\"100/second request limit reached - reduce calls per second or upgrade your account at quicknode.com\"}";
194        let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
195        assert!(TransportError::ErrorResp(err).is_retryable());
196    }
197
198    #[test]
199    fn test_retry_error_429() {
200        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."}"#;
201        let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
202        assert!(TransportError::ErrorResp(err).is_retryable());
203    }
204}