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 const fn is_pubsub_unavailable(&self) -> bool {
83 matches!(self, Self::PubsubUnavailable)
84 }
85
86 pub const fn is_backend_gone(&self) -> bool {
88 matches!(self, Self::BackendGone)
89 }
90
91 pub const fn is_http_error(&self) -> bool {
93 matches!(self, Self::HttpError(_))
94 }
95
96 pub const fn as_http_error(&self) -> Option<&HttpError> {
98 match self {
99 Self::HttpError(err) => Some(err),
100 _ => None,
101 }
102 }
103
104 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 pub fn is_retry_err(&self) -> bool {
115 match self {
116 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#[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 pub status: u16,
139 pub body: String,
141}
142
143impl HttpError {
144 pub const fn is_rate_limit_err(&self) -> bool {
146 self.status == 429
147 }
148
149 pub const fn is_temporarily_unavailable(&self) -> bool {
152 self.status == 503
153 }
154}
155
156pub(crate) trait RpcErrorExt {
158 fn is_retryable(&self) -> bool;
160
161 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 Self::Transport(err) => err.is_retry_err(),
171 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 #[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 let backoff_seconds = &data["rate"]["backoff_seconds"];
205 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}