1use std::fmt;
4
5use thiserror::Error;
6
7#[derive(Debug, Error)]
10pub enum AvailabilityError {
11 #[error("{0}")]
12 NotFound(String),
13 #[error("{0}")]
14 RangeExceeded(String),
15 #[error("{0}")]
16 BadRequest(String),
17 #[error("{0}")]
19 Pruned(String),
20}
21
22#[derive(Debug, Error)]
24pub enum SubmitError {
25 #[error("{0}")]
27 Invalid(String),
28 #[error("{0}")]
30 Overloaded(String),
31}
32
33#[derive(Debug)]
35pub enum ApiError {
36 BadRequest(anyhow::Error),
38 NotFound(anyhow::Error),
40 Gone(anyhow::Error),
42 Internal(anyhow::Error),
44 Unavailable(anyhow::Error),
46}
47
48impl fmt::Display for ApiError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 ApiError::BadRequest(err)
52 | ApiError::NotFound(err)
53 | ApiError::Gone(err)
54 | ApiError::Internal(err)
55 | ApiError::Unavailable(err) => {
56 f.write_str(&espresso_utils::redact::scrub(&err.to_string()))
60 },
61 }
62 }
63}
64
65impl std::error::Error for ApiError {
66 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
67 match self {
68 ApiError::BadRequest(err)
69 | ApiError::NotFound(err)
70 | ApiError::Gone(err)
71 | ApiError::Internal(err)
72 | ApiError::Unavailable(err) => err.source(),
73 }
74 }
75}
76
77pub fn classify(err: anyhow::Error) -> ApiError {
82 match err.downcast_ref::<SubmitError>() {
83 Some(SubmitError::Invalid(_)) => return ApiError::BadRequest(err),
84 Some(SubmitError::Overloaded(_)) => return ApiError::Unavailable(err),
85 None => {},
86 }
87 match err.downcast_ref::<AvailabilityError>() {
88 Some(AvailabilityError::NotFound(_)) => ApiError::NotFound(err),
89 Some(AvailabilityError::Pruned(_)) => ApiError::Gone(err),
90 Some(AvailabilityError::RangeExceeded(_) | AvailabilityError::BadRequest(_)) => {
91 ApiError::BadRequest(err)
92 },
93 None => ApiError::Internal(err),
94 }
95}
96
97pub fn to_status(err: anyhow::Error) -> tonic::Status {
101 let err = classify(err);
102 let message = err.to_string();
103 match err {
104 ApiError::NotFound(_) | ApiError::Gone(_) => tonic::Status::not_found(message),
105 ApiError::BadRequest(_) => tonic::Status::invalid_argument(message),
106 ApiError::Internal(_) => tonic::Status::internal(message),
107 ApiError::Unavailable(_) => tonic::Status::unavailable(message),
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
119 fn to_status_scrubs_provider_credentials() {
120 let err = anyhow::anyhow!(
121 r#"failed to get total supply: reqwest::Error {{ url: "https://u:p@rpc.invalid/v1/FAKEKEY" }}"#
122 );
123
124 let status = to_status(err);
125
126 assert_eq!(status.code(), tonic::Code::Internal);
127 assert!(
128 !status.message().contains("FAKEKEY"),
129 "{}",
130 status.message()
131 );
132 assert!(!status.message().contains("u:p"), "{}", status.message());
133 assert!(
134 status.message().contains("rpc.invalid"),
135 "{}",
136 status.message()
137 );
138 }
139
140 #[test]
142 fn availability_not_found_maps_to_not_found() {
143 let status = to_status(anyhow::Error::new(AvailabilityError::NotFound(
144 "leaf 7".to_string(),
145 )));
146 assert_eq!(status.code(), tonic::Code::NotFound);
147 }
148
149 #[test]
150 fn availability_pruned_maps_to_gone() {
151 let err = classify(anyhow::Error::new(AvailabilityError::Pruned(
152 "state at height 7".to_string(),
153 )));
154 assert!(matches!(err, ApiError::Gone(_)), "{err}");
155 }
156}