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    /// Returns true if this is [`TransportErrorKind::PubsubUnavailable`].
82    pub const fn is_pubsub_unavailable(&self) -> bool {
83        matches!(self, Self::PubsubUnavailable)
84    }
85
86    /// Returns true if this is [`TransportErrorKind::BackendGone`].
87    pub const fn is_backend_gone(&self) -> bool {
88        matches!(self, Self::BackendGone)
89    }
90
91    /// Returns true if this is [`TransportErrorKind::HttpError`].
92    pub const fn is_http_error(&self) -> bool {
93        matches!(self, Self::HttpError(_))
94    }
95
96    /// Returns the [`HttpError`] if this is [`TransportErrorKind::HttpError`].
97    pub const fn as_http_error(&self) -> Option<&HttpError> {
98        match self {
99            Self::HttpError(err) => Some(err),
100            _ => None,
101        }
102    }
103
104    /// Returns the custom error if this is [`TransportErrorKind::Custom`].
105    pub const fn as_custom(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
106        match self {
107            Self::Custom(err) => Some(&**err),
108            _ => None,
109        }
110    }
111
112    /// Analyzes the [TransportErrorKind] and decides if the request should be retried based on the
113    /// variant.
114    pub fn is_retry_err(&self) -> bool {
115        match self {
116            // Missing batch response errors can be retried.
117            Self::MissingBatchResponse(_) => true,
118            Self::HttpError(http_err) => {
119                http_err.is_rate_limit_err() || http_err.is_temporarily_unavailable()
120            }
121            Self::Custom(err) => {
122                let msg = err.to_string();
123                msg.contains("429 Too Many Requests")
124            }
125            _ => false,
126        }
127    }
128}
129
130/// Type for holding HTTP errors such as 429 rate limit error.
131#[derive(Debug, thiserror::Error)]
132#[error(
133    "HTTP error {status} with {}",
134    if body.is_empty() { "empty body".to_string() } else { format!("body: {body}") }
135)]
136pub struct HttpError {
137    /// The HTTP status code.
138    pub status: u16,
139    /// The HTTP response body.
140    pub body: String,
141}
142
143impl HttpError {
144    /// Checks the `status` to determine whether the request should be retried.
145    pub const fn is_rate_limit_err(&self) -> bool {
146        self.status == 429
147    }
148
149    /// Checks the `status` to determine whether the service was temporarily unavailable and should
150    /// be retried.
151    pub const fn is_temporarily_unavailable(&self) -> bool {
152        self.status == 503
153    }
154}
155
156/// Extension trait to implement methods for [`RpcError<TransportErrorKind, E>`].
157pub(crate) trait RpcErrorExt {
158    /// Analyzes whether to retry the request depending on the error.
159    fn is_retryable(&self) -> bool;
160
161    /// Fetches the backoff hint from the error message if present
162    fn backoff_hint(&self) -> Option<std::time::Duration>;
163}
164
165impl RpcErrorExt for RpcError<TransportErrorKind> {
166    fn is_retryable(&self) -> bool {
167        match self {
168            // There was a transport-level error. This is either a non-retryable error,
169            // or a server error that should be retried.
170            Self::Transport(err) => err.is_retry_err(),
171            // The transport could not serialize the error itself. The request was malformed from
172            // the start.
173            Self::SerError(_) => false,
174            Self::DeserError { text, .. } => {
175                if let Ok(resp) = serde_json::from_str::<ErrorPayload>(text) {
176                    return resp.is_retry_err();
177                }
178
179                // some providers send invalid JSON RPC in the error case (no `id:u64`), but the
180                // text should be a `JsonRpcError`
181                #[derive(Deserialize)]
182                struct Resp {
183                    error: ErrorPayload,
184                }
185
186                if let Ok(resp) = serde_json::from_str::<Resp>(text) {
187                    return resp.error.is_retry_err();
188                }
189
190                false
191            }
192            Self::ErrorResp(err) => err.is_retry_err(),
193            Self::NullResp => true,
194            _ => false,
195        }
196    }
197
198    fn backoff_hint(&self) -> Option<std::time::Duration> {
199        if let Self::ErrorResp(resp) = self {
200            let data = resp.try_data_as::<serde_json::Value>();
201            if let Some(Ok(data)) = data {
202                // if daily rate limit exceeded, infura returns the requested backoff in the error
203                // response
204                let backoff_seconds = &data["rate"]["backoff_seconds"];
205                // infura rate limit error
206                if let Some(seconds) = backoff_seconds.as_u64() {
207                    return Some(std::time::Duration::from_secs(seconds));
208                }
209                if let Some(seconds) = backoff_seconds.as_f64() {
210                    return Some(std::time::Duration::from_secs(seconds as u64 + 1));
211                }
212            }
213        }
214        None
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_retry_error() {
224        let err = "{\"code\":-32007,\"message\":\"100/second request limit reached - reduce calls per second or upgrade your account at quicknode.com\"}";
225        let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
226        assert!(TransportError::ErrorResp(err).is_retryable());
227    }
228
229    #[test]
230    fn test_retry_error_429() {
231        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."}"#;
232        let err = serde_json::from_str::<ErrorPayload>(err).unwrap();
233        assert!(TransportError::ErrorResp(err).is_retryable());
234    }
235}