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 http::StatusCode;
18use std::error::Error;
19
20pub use gax::error::CredentialsError;
21
22/// Represents an error using [SubjectTokenProvider].
23///
24/// The Google Cloud client libraries may experience problems when
25/// fetching the subject token. For example, a temporary error may occur
26/// when [SubjectTokenProvider] tries to fetch the token from a third party service.
27///
28/// Applications rarely need to create instances of this error type. The
29/// exception may be when they are providing their own custom [SubjectTokenProvider]
30/// implementation.
31///
32/// # Example
33///
34/// ```
35/// # use std::error::Error;
36/// # use std::fmt;
37/// # use google_cloud_auth::errors::SubjectTokenProviderError;
38/// #[derive(Debug)]
39/// struct CustomTokenError {
40///     message: String,
41///     is_transient: bool,
42/// }
43///
44/// impl fmt::Display for CustomTokenError {
45///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46///         write!(f, "{}", self.message)
47///     }
48/// }
49///
50/// impl Error for CustomTokenError {}
51///
52/// impl SubjectTokenProviderError for CustomTokenError {
53///     fn is_transient(&self) -> bool {
54///         self.is_transient
55///     }
56/// }
57/// ```
58/// [SubjectTokenProvider]: crate::credentials::subject_token::SubjectTokenProvider
59pub trait SubjectTokenProviderError: Error + Send + Sync + 'static {
60    /// Return true if the error is transient and the call may succeed in the future.
61    ///
62    /// Applications should only return true if the error automatically
63    /// recovers, without the need for any human action.
64    ///
65    /// Timeouts and network problems are good candidates for `is_transient() == true`.
66    /// Configuration errors that require changing a file, or installing an executable are not.
67    fn is_transient(&self) -> bool;
68}
69
70impl SubjectTokenProviderError for CredentialsError {
71    fn is_transient(&self) -> bool {
72        self.is_transient()
73    }
74}
75
76pub(crate) fn from_http_error(err: reqwest::Error, msg: &str) -> CredentialsError {
77    let transient = self::is_retryable(&err);
78    CredentialsError::new(transient, msg, err)
79}
80
81pub(crate) async fn from_http_response(response: reqwest::Response, msg: &str) -> CredentialsError {
82    let err = response
83        .error_for_status_ref()
84        .expect_err("this function is only called on errors");
85    let body = response.text().await;
86    let transient = crate::errors::is_retryable(&err);
87    match body {
88        Err(e) => CredentialsError::new(transient, msg, e),
89        Ok(b) => CredentialsError::new(transient, format!("{msg}, body=<{b}>"), err),
90    }
91}
92
93/// A helper to create a non-retryable error.
94pub(crate) fn non_retryable<T: Error + Send + Sync + 'static>(source: T) -> CredentialsError {
95    CredentialsError::from_source(false, source)
96}
97
98pub(crate) fn non_retryable_from_str<T: Into<String>>(message: T) -> CredentialsError {
99    CredentialsError::from_msg(false, message)
100}
101
102fn is_retryable(err: &reqwest::Error) -> bool {
103    // Connection errors are transient more often than not. A bad configuration
104    // can point to a non-existing service, and that will never recover.
105    // However: (1) we expect this to be rare, and (2) this is what limiting
106    // retry policies and backoff policies handle.
107    if err.is_connect() {
108        return true;
109    }
110    match err.status() {
111        Some(code) => is_retryable_code(code),
112        None => false,
113    }
114}
115
116fn is_retryable_code(code: StatusCode) -> bool {
117    match code {
118        // Internal server errors do not indicate that there is anything wrong
119        // with our request, so we retry them.
120        StatusCode::INTERNAL_SERVER_ERROR
121        | StatusCode::SERVICE_UNAVAILABLE
122        | StatusCode::REQUEST_TIMEOUT
123        | StatusCode::TOO_MANY_REQUESTS => true,
124        _ => false,
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::num::ParseIntError;
132    use test_case::test_case;
133
134    #[test_case(StatusCode::INTERNAL_SERVER_ERROR)]
135    #[test_case(StatusCode::SERVICE_UNAVAILABLE)]
136    #[test_case(StatusCode::REQUEST_TIMEOUT)]
137    #[test_case(StatusCode::TOO_MANY_REQUESTS)]
138    fn retryable(c: StatusCode) {
139        assert!(is_retryable_code(c));
140    }
141
142    #[test_case(StatusCode::NOT_FOUND)]
143    #[test_case(StatusCode::UNAUTHORIZED)]
144    #[test_case(StatusCode::BAD_REQUEST)]
145    #[test_case(StatusCode::BAD_GATEWAY)]
146    #[test_case(StatusCode::PRECONDITION_FAILED)]
147    fn non_retryable(c: StatusCode) {
148        assert!(!is_retryable_code(c));
149    }
150
151    #[test]
152    fn helpers() {
153        let e = super::non_retryable_from_str("test-only-err-123");
154        assert!(!e.is_transient(), "{e}");
155        let got = format!("{e}");
156        assert!(got.contains("test-only-err-123"), "{got}");
157
158        let input = "NaN".parse::<u32>().unwrap_err();
159        let e = super::non_retryable(input.clone());
160        assert!(!e.is_transient(), "{e:?}");
161        let source = e.source().and_then(|e| e.downcast_ref::<ParseIntError>());
162        assert!(matches!(source, Some(ParseIntError { .. })), "{e:?}");
163    }
164}