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
44impl TransportErrorKind {
45 pub const fn recoverable(&self) -> bool {
48 matches!(self, Self::MissingBatchResponse(_))
49 }
50
51 pub fn custom_str(err: &str) -> TransportError {
53 RpcError::Transport(Self::Custom(err.into()))
54 }
55
56 pub fn custom(err: impl StdError + Send + Sync + 'static) -> TransportError {
58 RpcError::Transport(Self::Custom(Box::new(err)))
59 }
60
61 pub const fn missing_batch_response(id: Id) -> TransportError {
63 RpcError::Transport(Self::MissingBatchResponse(id))
64 }
65
66 pub const fn backend_gone() -> TransportError {
68 RpcError::Transport(Self::BackendGone)
69 }
70
71 pub const fn pubsub_unavailable() -> TransportError {
73 RpcError::Transport(Self::PubsubUnavailable)
74 }
75
76 pub const fn http_error(status: u16, body: String) -> TransportError {
78 RpcError::Transport(Self::HttpError(HttpError { status, body }))
79 }
80
81 pub fn is_retry_err(&self) -> bool {
84 match self {
85 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#[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 pub status: u16,
108 pub body: String,
110}
111
112impl HttpError {
113 pub const fn is_rate_limit_err(&self) -> bool {
115 self.status == 429
116 }
117
118 pub const fn is_temporarily_unavailable(&self) -> bool {
121 self.status == 503
122 }
123}
124
125pub(crate) trait RpcErrorExt {
127 fn is_retryable(&self) -> bool;
129
130 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 Self::Transport(err) => err.is_retry_err(),
140 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 #[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 let backoff_seconds = &data["rate"]["backoff_seconds"];
174 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}