google_cloud_auth/
errors.rs1use google_cloud_gax::error::Error as GaxError;
18use http::StatusCode;
19use std::error::Error;
20
21pub use google_cloud_gax::error::CredentialsError;
22
23pub trait SubjectTokenProviderError: Error + Send + Sync + 'static {
61 fn is_transient(&self) -> bool;
69}
70
71impl SubjectTokenProviderError for CredentialsError {
72 fn is_transient(&self) -> bool {
73 self.is_transient()
74 }
75}
76
77pub(crate) fn from_gax_error(err: GaxError, msg: &str) -> CredentialsError {
78 let transient = is_gax_error_retryable(&err);
79 CredentialsError::new(transient, msg, err)
80}
81
82pub(crate) fn from_http_error(err: reqwest::Error, msg: &str) -> CredentialsError {
83 let transient = self::is_retryable(&err);
84 CredentialsError::new(transient, msg, err)
85}
86
87pub(crate) async fn from_http_response(response: reqwest::Response, msg: &str) -> CredentialsError {
88 let err = response
89 .error_for_status_ref()
90 .expect_err("this function is only called on errors");
91 let body = response.text().await;
92 let transient = crate::errors::is_retryable(&err);
93 match body {
94 Err(e) => CredentialsError::new(transient, msg, e),
95 Ok(b) => CredentialsError::new(transient, format!("{msg}, body=<{b}>"), err),
96 }
97}
98
99pub(crate) fn non_retryable<T: Error + Send + Sync + 'static>(source: T) -> CredentialsError {
101 CredentialsError::from_source(false, source)
102}
103
104pub(crate) fn non_retryable_from_str<T: Into<String>>(message: T) -> CredentialsError {
105 CredentialsError::from_msg(false, message)
106}
107
108pub(crate) fn is_gax_error_retryable(err: &GaxError) -> bool {
109 if err
110 .http_status_code()
111 .and_then(|c| StatusCode::from_u16(c).ok())
112 .is_some_and(is_retryable_code)
113 {
114 return true;
115 }
116
117 let Some(s) = err.source() else { return false };
118
119 if let Some(cred_err) = s.downcast_ref::<CredentialsError>() {
120 return cred_err.is_transient();
121 }
122 if let Some(req_err) = s.downcast_ref::<reqwest::Error>() {
123 return is_retryable(req_err);
124 }
125 false
126}
127
128fn is_retryable(err: &reqwest::Error) -> bool {
129 if err.is_connect() {
134 return true;
135 }
136 match err.status() {
137 Some(code) => is_retryable_code(code),
138 None => false,
139 }
140}
141
142fn is_retryable_code(code: StatusCode) -> bool {
143 match code {
144 StatusCode::INTERNAL_SERVER_ERROR
147 | StatusCode::SERVICE_UNAVAILABLE
148 | StatusCode::REQUEST_TIMEOUT
149 | StatusCode::TOO_MANY_REQUESTS => true,
150 _ => false,
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use bytes::Bytes;
158 use http::HeaderMap;
159 use std::num::ParseIntError;
160 use test_case::test_case;
161
162 #[test_case(StatusCode::INTERNAL_SERVER_ERROR)]
163 #[test_case(StatusCode::SERVICE_UNAVAILABLE)]
164 #[test_case(StatusCode::REQUEST_TIMEOUT)]
165 #[test_case(StatusCode::TOO_MANY_REQUESTS)]
166 fn retryable(c: StatusCode) {
167 assert!(is_retryable_code(c));
168 }
169
170 #[test_case(StatusCode::NOT_FOUND)]
171 #[test_case(StatusCode::UNAUTHORIZED)]
172 #[test_case(StatusCode::BAD_REQUEST)]
173 #[test_case(StatusCode::BAD_GATEWAY)]
174 #[test_case(StatusCode::PRECONDITION_FAILED)]
175 fn non_retryable(c: StatusCode) {
176 assert!(!is_retryable_code(c));
177 }
178
179 #[test]
180 fn helpers() {
181 let e = super::non_retryable_from_str("test-only-err-123");
182 assert!(!e.is_transient(), "{e}");
183 let got = format!("{e}");
184 assert!(got.contains("test-only-err-123"), "{got}");
185
186 let input = "NaN".parse::<u32>().unwrap_err();
187 let e = super::non_retryable(input.clone());
188 assert!(!e.is_transient(), "{e:?}");
189 let source = e.source().and_then(|e| e.downcast_ref::<ParseIntError>());
190 assert!(matches!(source, Some(ParseIntError { .. })), "{e:?}");
191 }
192
193 #[test_case(GaxError::http(503, HeaderMap::new(), Bytes::from("test")), true ; "retryable http status")]
194 #[test_case(GaxError::http(404, HeaderMap::new(), Bytes::from("test")), false ; "non-retryable http status")]
195 #[test_case(GaxError::authentication(CredentialsError::new(true, "msg", "NaN".parse::<u32>().unwrap_err())), true ; "transient credentials error")]
196 #[test_case(GaxError::authentication(CredentialsError::new(false, "msg", "NaN".parse::<u32>().unwrap_err())), false ; "permanent credentials error")]
197 #[test_case(GaxError::io("some io error"), false ; "io error fallback")]
198 #[test_case(GaxError::timeout("timeout"), false ; "timeout fallback")]
199 fn test_is_gax_error_retryable(gax_err: GaxError, expected: bool) {
200 let cred_err = from_gax_error(gax_err, "some msg");
201 assert_eq!(cred_err.is_transient(), expected);
202 }
203}