Skip to main content

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    /// Deterministic, terminal failure that retry loops should not retry.
44    ///
45    /// Use this when the operation is guaranteed to fail again on retry —
46    /// for example a malformed request, a permission error, or any other
47    /// protocol-level failure that is not a transient socket / IO problem.
48    #[error("{0}")]
49    NonRetryable(#[source] Box<dyn StdError + Send + Sync + 'static>),
50}
51
52impl TransportErrorKind {
53    /// Returns `true` if the error is potentially recoverable.
54    /// This is a naive heuristic and should be used with caution.
55    pub const fn recoverable(&self) -> bool {
56        matches!(self, Self::MissingBatchResponse(_))
57    }
58
59    /// Instantiate a new `TransportError` from a custom error.
60    pub fn custom_str(err: &str) -> TransportError {
61        RpcError::Transport(Self::Custom(err.into()))
62    }
63
64    /// Instantiate a new `TransportError` from a custom error.
65    pub fn custom(err: impl StdError + Send + Sync + 'static) -> TransportError {
66        RpcError::Transport(Self::Custom(Box::new(err)))
67    }
68
69    /// Instantiate a new non-retryable `TransportError` from a string.
70    pub fn non_retryable_str(err: &str) -> TransportError {
71        RpcError::Transport(Self::NonRetryable(err.into()))
72    }
73
74    /// Instantiate a new non-retryable `TransportError` from a custom error.
75    pub fn non_retryable(err: impl StdError + Send + Sync + 'static) -> TransportError {
76        RpcError::Transport(Self::NonRetryable(Box::new(err)))
77    }
78
79    /// Returns true if this is [`TransportErrorKind::NonRetryable`].
80    pub const fn is_non_retryable(&self) -> bool {
81        matches!(self, Self::NonRetryable(_))
82    }
83
84    /// Instantiate a new `TransportError` from a missing ID.
85    pub const fn missing_batch_response(id: Id) -> TransportError {
86        RpcError::Transport(Self::MissingBatchResponse(id))
87    }
88
89    /// Instantiate a new `TransportError::BackendGone`.
90    pub const fn backend_gone() -> TransportError {
91        RpcError::Transport(Self::BackendGone)
92    }
93
94    /// Instantiate a new `TransportError::PubsubUnavailable`.
95    pub const fn pubsub_unavailable() -> TransportError {
96        RpcError::Transport(Self::PubsubUnavailable)
97    }
98
99    /// Instantiate a new `TransportError::HttpError`.
100    pub const fn http_error(status: u16, body: String) -> TransportError {
101        RpcError::Transport(Self::HttpError(HttpError { status, body }))
102    }
103
104    /// Returns true if this is [`TransportErrorKind::PubsubUnavailable`].
105    pub const fn is_pubsub_unavailable(&self) -> bool {
106        matches!(self, Self::PubsubUnavailable)
107    }
108
109    /// Returns true if this is [`TransportErrorKind::BackendGone`].
110    pub const fn is_backend_gone(&self) -> bool {
111        matches!(self, Self::BackendGone)
112    }
113
114    /// Returns true if this is [`TransportErrorKind::HttpError`].
115    pub const fn is_http_error(&self) -> bool {
116        matches!(self, Self::HttpError(_))
117    }
118
119    /// Returns the [`HttpError`] if this is [`TransportErrorKind::HttpError`].
120    pub const fn as_http_error(&self) -> Option<&HttpError> {
121        match self {
122            Self::HttpError(err) => Some(err),
123            _ => None,
124        }
125    }
126
127    /// Returns the custom error if this is [`TransportErrorKind::Custom`].
128    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    /// Analyzes the [TransportErrorKind] and decides if the request should be retried based on the
136    /// variant.
137    pub fn is_retry_err(&self) -> bool {
138        match self {
139            // Missing batch response errors can be retried.
140            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/// Type for holding HTTP errors such as 429 rate limit error.
154#[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    /// The HTTP status code.
161    pub status: u16,
162    /// The HTTP response body.
163    pub body: String,
164}
165
166impl HttpError {
167    /// Checks the `status` to determine whether the request should be retried.
168    pub const fn is_rate_limit_err(&self) -> bool {
169        self.status == 429
170    }
171
172    /// Checks the `status` to determine whether the service was temporarily unavailable and should
173    /// be retried.
174    pub const fn is_temporarily_unavailable(&self) -> bool {
175        self.status == 503
176    }
177}
178
179/// Extension trait to implement methods for [`RpcError<TransportErrorKind, E>`].
180pub(crate) trait RpcErrorExt {
181    /// Analyzes whether to retry the request depending on the error.
182    fn is_retryable(&self) -> bool;
183
184    /// Fetches the backoff hint from the error message if present
185    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            // There was a transport-level error. This is either a non-retryable error,
192            // or a server error that should be retried.
193            Self::Transport(err) => err.is_retry_err(),
194            // The transport could not serialize the error itself. The request was malformed from
195            // the start.
196            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                // some providers send invalid JSON RPC in the error case (no `id:u64`), but the
203                // text should be a `JsonRpcError`
204                #[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            // try to extract backoff from the error data (infura-style)
224            let data = resp.try_data_as::<serde_json::Value>();
225            if let Some(Ok(data)) = data {
226                // if daily rate limit exceeded, infura returns the requested backoff in the error
227                // response
228                let backoff_seconds = &data["rate"]["backoff_seconds"];
229                // infura rate limit error
230                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            // try to extract backoff from the error message, e.g. "try again in 4ms"
239            if let Some(duration) = parse_retry_after(&resp.message) {
240                return Some(duration);
241            }
242        }
243        None
244    }
245}
246
247/// Parses a duration from messages like "try again in 4ms", "try again in 1s".
248fn 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}