alloy_json_rpc/error.rs
1use crate::{ErrorPayload, RpcRecv};
2use alloy_primitives::B256;
3use serde_json::value::RawValue;
4
5/// A transport error that may carry a JSON-RPC error in its raw response body.
6///
7/// [`RpcError`] is generic over its transport error, which is defined by downstream transport
8/// crates and therefore opaque here. This trait is the hook those crates implement so that
9/// [`RpcError::error_code`] can reach JSON-RPC errors that never made it through JSON-RPC decoding,
10/// such as an error served with a non-2xx HTTP status.
11///
12/// Implementors own the parsing, since they own the wire format their body arrives in.
13pub trait ErrorResponseCode {
14 /// Returns the JSON-RPC error code carried in this error's raw response body, if the body
15 /// contains a JSON-RPC error at all.
16 ///
17 /// Returns `None` when the error carries no body, or when the body is not a JSON-RPC error.
18 fn error_response_code(&self) -> Option<i64>;
19}
20
21/// An RPC error.
22#[derive(Debug, thiserror::Error)]
23pub enum RpcError<E, ErrResp = Box<RawValue>> {
24 /// Server returned an error response.
25 #[error("server returned an error response: {0}")]
26 ErrorResp(ErrorPayload<ErrResp>),
27
28 /// Server returned a null response when a non-null response was expected.
29 #[error("server returned a null response when a non-null response was expected")]
30 NullResp,
31
32 /// Rpc server returned an unsupported feature.
33 #[error("unsupported feature: {0}")]
34 UnsupportedFeature(&'static str),
35
36 /// Returned when a local pre-processing step fails. This allows custom
37 /// errors from local signers or request pre-processors.
38 #[error("local usage error: {0}")]
39 LocalUsageError(#[source] Box<dyn std::error::Error + Send + Sync>),
40
41 /// JSON serialization error.
42 #[error("serialization error: {0}")]
43 SerError(
44 /// The underlying serde_json error.
45 // To avoid accidentally confusing ser and deser errors, we do not use
46 // the `#[from]` tag.
47 #[source]
48 serde_json::Error,
49 ),
50 /// JSON deserialization error.
51 #[error("deserialization error: {err}\n{text}")]
52 DeserError {
53 /// The underlying serde_json error.
54 // To avoid accidentally confusing ser and deser errors, we do not use
55 // the `#[from]` tag.
56 #[source]
57 err: serde_json::Error,
58 /// For deser errors, the text that failed to deserialize.
59 text: String,
60 },
61
62 /// Transport error.
63 ///
64 /// This variant is used when the error occurs during communication.
65 #[error(transparent)]
66 Transport(
67 /// The underlying transport error.
68 #[from]
69 E,
70 ),
71}
72
73impl<E, ErrResp> RpcError<E, ErrResp>
74where
75 ErrResp: RpcRecv,
76{
77 /// Instantiate a new `ErrorResp` from an error response.
78 pub const fn err_resp(err: ErrorPayload<ErrResp>) -> Self {
79 Self::ErrorResp(err)
80 }
81
82 /// Instantiate a new `LocalUsageError` from a custom error.
83 pub fn local_usage(err: impl std::error::Error + Send + Sync + 'static) -> Self {
84 Self::LocalUsageError(err.into())
85 }
86
87 /// Instantiate a new `LocalUsageError` from a custom error message.
88 pub fn local_usage_str(err: &str) -> Self {
89 Self::LocalUsageError(err.into())
90 }
91
92 /// Instantiate a new `DeserError` from a [`serde_json::Error`] and the
93 /// text. This should be called when the error occurs during
94 /// deserialization.
95 ///
96 /// Note: This will check if the response is actually an [ErrorPayload], if so it will return a
97 /// [RpcError::ErrorResp].
98 pub fn deser_err(err: serde_json::Error, text: impl AsRef<str>) -> Self {
99 let text = text.as_ref();
100
101 // check if the response is actually an `ErrorPayload`
102 if let Ok(err) = serde_json::from_str::<ErrorPayload<ErrResp>>(text) {
103 return Self::ErrorResp(err);
104 }
105
106 Self::DeserError { err, text: text.to_owned() }
107 }
108}
109
110impl<E, ErrResp> RpcError<E, ErrResp> {
111 /// Instantiate a new `SerError` from a [`serde_json::Error`]. This
112 /// should be called when the error occurs during serialization.
113 pub const fn ser_err(err: serde_json::Error) -> Self {
114 Self::SerError(err)
115 }
116
117 /// Check if the error is a serialization error.
118 pub const fn is_ser_error(&self) -> bool {
119 matches!(self, Self::SerError(_))
120 }
121
122 /// Check if the error is a deserialization error.
123 pub const fn is_deser_error(&self) -> bool {
124 matches!(self, Self::DeserError { .. })
125 }
126
127 /// Check if the error is a transport error.
128 pub const fn is_transport_error(&self) -> bool {
129 matches!(self, Self::Transport(_))
130 }
131
132 /// Check if the error is an error response.
133 pub const fn is_error_resp(&self) -> bool {
134 matches!(self, Self::ErrorResp(_))
135 }
136
137 /// Check if the error is a null response.
138 pub const fn is_null_resp(&self) -> bool {
139 matches!(self, Self::NullResp)
140 }
141
142 /// Check if the error is an unsupported feature error.
143 pub const fn is_unsupported_feature(&self) -> bool {
144 matches!(self, Self::UnsupportedFeature(_))
145 }
146
147 /// Check if the error is a local usage error.
148 pub const fn is_local_usage_error(&self) -> bool {
149 matches!(self, Self::LocalUsageError(_))
150 }
151
152 /// Fallible conversion to an error response.
153 pub const fn as_error_resp(&self) -> Option<&ErrorPayload<ErrResp>> {
154 match self {
155 Self::ErrorResp(err) => Some(err),
156 _ => None,
157 }
158 }
159
160 /// Returns the transport error if this is a [`RpcError::Transport`]
161 pub const fn as_transport_err(&self) -> Option<&E> {
162 match self {
163 Self::Transport(err) => Some(err),
164 _ => None,
165 }
166 }
167}
168
169impl<E: ErrorResponseCode, ErrResp> RpcError<E, ErrResp> {
170 /// Returns the JSON-RPC error code this error reports, if any.
171 ///
172 /// This covers regular JSON-RPC error responses as well as JSON-RPC errors that a provider
173 /// buried inside a transport-level error response, for example a `-32601` served with an HTTP
174 /// 403. The latter never reach [`RpcError::ErrorResp`], so [`RpcError::as_error_resp`] alone
175 /// misses them.
176 ///
177 /// # Example
178 ///
179 /// ```rust
180 /// use alloy_json_rpc::{ErrorPayload, RpcError};
181 ///
182 /// # struct NoBody;
183 /// # impl alloy_json_rpc::ErrorResponseCode for NoBody {
184 /// # fn error_response_code(&self) -> Option<i64> {
185 /// # None
186 /// # }
187 /// # }
188 /// let error: RpcError<NoBody> = RpcError::ErrorResp(ErrorPayload::method_not_found());
189 /// assert_eq!(error.error_code(), Some(-32601));
190 /// ```
191 pub fn error_code(&self) -> Option<i64> {
192 if let Some(payload) = self.as_error_resp() {
193 return Some(payload.code);
194 }
195 self.as_transport_err()?.error_response_code()
196 }
197}
198
199impl<E> RpcError<E, Box<RawValue>> {
200 /// Parses the error data field as a hex string of specified length.
201 ///
202 /// Returns `Some(T)` if the data contains a valid hex string of the expected length.
203 fn parse_data<T: std::str::FromStr>(&self) -> Option<T> {
204 let error_payload = self.as_error_resp()?;
205 let data = error_payload.data.as_ref()?;
206 let data_str = data.get().trim_matches('"').trim();
207 data_str.parse().ok()
208 }
209
210 /// Extracts a transaction hash from the error data field.
211 ///
212 /// Useful for EIP-7966 `eth_sendRawTransactionSync` errors that return
213 /// the transaction hash even when the transaction fails.
214 ///
215 /// Returns `Some(hash)` if the data contains a valid 32-byte hex string.
216 ///
217 /// # Example
218 ///
219 /// ```rust
220 /// use alloy_json_rpc::{RpcError, ErrorPayload};
221 /// use alloy_primitives::B256;
222 ///
223 /// // Simulate an EIP-7966 error response
224 /// let json = r#"{"code":5,"message":"insufficient funds","data":"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}"#;
225 /// let error_payload: ErrorPayload = serde_json::from_str(json).unwrap();
226 /// let rpc_error: RpcError<(), _> = RpcError::ErrorResp(error_payload);
227 ///
228 /// if let Some(tx_hash) = rpc_error.tx_hash_data() {
229 /// println!("Transaction hash: {}", tx_hash);
230 /// }
231 /// ```
232 pub fn tx_hash_data(&self) -> Option<B256> {
233 self.parse_data()
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 /// A stand-in transport error that reports a JSON-RPC code from its response body.
242 struct Body(Option<i64>);
243
244 impl ErrorResponseCode for Body {
245 fn error_response_code(&self) -> Option<i64> {
246 self.0
247 }
248 }
249
250 fn transport(code: Option<i64>) -> RpcError<Body> {
251 RpcError::Transport(Body(code))
252 }
253
254 #[test]
255 fn error_code_from_error_response() {
256 let error: RpcError<Body> = RpcError::ErrorResp(ErrorPayload::invalid_request());
257 assert_eq!(error.error_code(), Some(-32600));
258 }
259
260 #[test]
261 fn error_code_from_transport_body() {
262 assert_eq!(transport(Some(-32601)).error_code(), Some(-32601));
263 }
264
265 #[test]
266 fn error_code_none_for_unrelated_errors() {
267 assert_eq!(transport(None).error_code(), None);
268 assert_eq!(RpcError::<Body>::NullResp.error_code(), None);
269 }
270
271 #[test]
272 fn error_code_is_callable_generically() {
273 fn code<E: ErrorResponseCode, ErrResp>(err: &RpcError<E, ErrResp>) -> Option<i64> {
274 err.error_code()
275 }
276
277 assert_eq!(code(&transport(Some(-32602))), Some(-32602));
278 }
279}