1use crate::graphql::GraphQlError;
6use std::fmt;
7
8const RETRYABLE_STATUSES: &[u16] = &[429, 500, 502, 503, 504];
10
11pub type Result<T> = std::result::Result<T, Error>;
13
14#[derive(Debug, thiserror::Error)]
16pub enum Error {
17 #[error("config error: {0}")]
18 Config(String),
19
20 #[error("http error: {0}")]
21 Http(#[from] reqwest::Error),
22
23 #[error("url error: {0}")]
24 Url(#[from] url::ParseError),
25
26 #[error("json error: {0}")]
27 Json(#[from] serde_json::Error),
28
29 #[error("graphql error: {message}")]
30 GraphQl {
31 status: Option<u16>,
33 errors: Vec<GraphQlError>,
35 body: String,
37 message: String,
39 },
40}
41
42impl Error {
43 pub fn is_auth_error(&self) -> bool {
45 matches!(
46 self,
47 Error::GraphQl {
48 status: Some(401 | 403),
49 ..
50 }
51 ) || matches!(self, Error::Http(err) if err.status() == Some(reqwest::StatusCode::UNAUTHORIZED))
52 }
53
54 pub fn is_retryable(&self) -> bool {
60 match self {
61 Error::Config(_) | Error::Url(_) | Error::Json(_) => false,
62 Error::Http(err) => {
63 if err.is_timeout() || err.is_connect() {
64 return true;
65 }
66 match err.status() {
67 Some(status) => {
68 status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS
69 }
70 None => true,
72 }
73 }
74 Error::GraphQl { status, .. } => {
75 status.is_some_and(|s| RETRYABLE_STATUSES.contains(&s))
76 }
77 }
78 }
79}
80
81impl fmt::Display for GraphQlError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(f, "{}", self.message)
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_is_auth_error() {
93 let err = Error::GraphQl {
94 status: Some(401),
95 errors: vec![],
96 body: String::new(),
97 message: "unauthorized".to_string(),
98 };
99 assert!(err.is_auth_error());
100
101 let err = Error::GraphQl {
102 status: Some(403),
103 errors: vec![],
104 body: String::new(),
105 message: "forbidden".to_string(),
106 };
107 assert!(err.is_auth_error());
108
109 let err = Error::GraphQl {
110 status: Some(500),
111 errors: vec![],
112 body: String::new(),
113 message: "server error".to_string(),
114 };
115 assert!(!err.is_auth_error());
116 }
117
118 #[test]
119 fn test_non_graphql_errors_not_auth() {
120 let config_err = Error::Config("bad".into());
121 assert!(!config_err.is_auth_error());
122
123 let url_err: Error = url::Url::parse(":::").unwrap_err().into();
124 assert!(!url_err.is_auth_error());
125
126 let json_err: Error = serde_json::from_str::<serde_json::Value>("!!!")
127 .unwrap_err()
128 .into();
129 assert!(!json_err.is_auth_error());
130 }
131
132 #[test]
133 fn test_is_retryable_server_errors() {
134 for status in [429, 500, 502, 503, 504] {
135 let err = Error::GraphQl {
136 status: Some(status),
137 errors: vec![],
138 body: String::new(),
139 message: "server error".to_string(),
140 };
141 assert!(err.is_retryable(), "status {status} should be retryable");
142 }
143 }
144
145 #[test]
146 fn test_is_not_retryable_client_errors() {
147 for status in [400, 401, 403, 404, 422] {
148 let err = Error::GraphQl {
149 status: Some(status),
150 errors: vec![],
151 body: String::new(),
152 message: "client error".to_string(),
153 };
154 assert!(
155 !err.is_retryable(),
156 "status {status} should not be retryable"
157 );
158 }
159 }
160
161 #[test]
162 fn test_is_not_retryable_config_url_json() {
163 let config_err = Error::Config("bad".into());
164 assert!(!config_err.is_retryable());
165
166 let url_err: Error = url::Url::parse(":::").unwrap_err().into();
167 assert!(!url_err.is_retryable());
168
169 let json_err: Error = serde_json::from_str::<serde_json::Value>("!!!")
170 .unwrap_err()
171 .into();
172 assert!(!json_err.is_retryable());
173 }
174}