Skip to main content

google_cloud_auth/
errors.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Common errors generated by the components in this crate.
16
17use google_cloud_gax::error::Error as GaxError;
18use http::StatusCode;
19use std::error::Error;
20
21pub use google_cloud_gax::error::CredentialsError;
22
23/// Represents an error using [SubjectTokenProvider].
24///
25/// The Google Cloud client libraries may experience problems when
26/// fetching the subject token. For example, a temporary error may occur
27/// when [SubjectTokenProvider] tries to fetch the token from a third party service.
28///
29/// Applications rarely need to create instances of this error type. The
30/// exception may be when they are providing their own custom [SubjectTokenProvider]
31/// implementation.
32///
33/// # Example
34///
35/// ```
36/// # use std::error::Error;
37/// # use std::fmt;
38/// # use google_cloud_auth::errors::SubjectTokenProviderError;
39/// #[derive(Debug)]
40/// struct CustomTokenError {
41///     message: String,
42///     is_transient: bool,
43/// }
44///
45/// impl fmt::Display for CustomTokenError {
46///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47///         write!(f, "{}", self.message)
48///     }
49/// }
50///
51/// impl Error for CustomTokenError {}
52///
53/// impl SubjectTokenProviderError for CustomTokenError {
54///     fn is_transient(&self) -> bool {
55///         self.is_transient
56///     }
57/// }
58/// ```
59/// [SubjectTokenProvider]: crate::credentials::subject_token::SubjectTokenProvider
60pub trait SubjectTokenProviderError: Error + Send + Sync + 'static {
61    /// Return true if the error is transient and the call may succeed in the future.
62    ///
63    /// Applications should only return true if the error automatically
64    /// recovers, without the need for any human action.
65    ///
66    /// Timeouts and network problems are good candidates for `is_transient() == true`.
67    /// Configuration errors that require changing a file, or installing an executable are not.
68    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
99/// A helper to create a non-retryable error.
100pub(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    // Connection errors are transient more often than not. A bad configuration
130    // can point to a non-existing service, and that will never recover.
131    // However: (1) we expect this to be rare, and (2) this is what limiting
132    // retry policies and backoff policies handle.
133    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        // Internal server errors do not indicate that there is anything wrong
145        // with our request, so we retry them.
146        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}