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
22pub(crate) fn from_http_error(err: reqwest::Error, msg: &str) -> CredentialsError {
23    let transient = self::is_retryable(&err);
24    CredentialsError::new(transient, msg, err)
25}
26
27pub(crate) async fn from_http_response(response: reqwest::Response, msg: &str) -> CredentialsError {
28    let err = response
29        .error_for_status_ref()
30        .expect_err("this function is only called on errors");
31    let body = response.text().await;
32    let transient = crate::errors::is_retryable(&err);
33    match body {
34        Err(e) => CredentialsError::new(transient, msg, e),
35        Ok(b) => CredentialsError::new(transient, format!("{msg}, body=<{b}>"), err),
36    }
37}
38
39/// A helper to create a non-retryable error.
40pub(crate) fn non_retryable<T: Error + Send + Sync + 'static>(source: T) -> CredentialsError {
41    CredentialsError::from_source(false, source)
42}
43
44pub(crate) fn non_retryable_from_str<T: Into<String>>(message: T) -> CredentialsError {
45    CredentialsError::from_msg(false, message)
46}
47
48fn is_retryable(err: &reqwest::Error) -> bool {
49    // Connection errors are transient more often than not. A bad configuration
50    // can point to a non-existing service, and that will never recover.
51    // However: (1) we expect this to be rare, and (2) this is what limiting
52    // retry policies and backoff policies handle.
53    if err.is_connect() {
54        return true;
55    }
56    match err.status() {
57        Some(code) => is_retryable_code(code),
58        None => false,
59    }
60}
61
62fn is_retryable_code(code: StatusCode) -> bool {
63    match code {
64        // Internal server errors do not indicate that there is anything wrong
65        // with our request, so we retry them.
66        StatusCode::INTERNAL_SERVER_ERROR
67        | StatusCode::SERVICE_UNAVAILABLE
68        | StatusCode::REQUEST_TIMEOUT
69        | StatusCode::TOO_MANY_REQUESTS => true,
70        _ => false,
71    }
72}
73
74#[cfg(test)]
75mod test {
76    use super::*;
77    use std::num::ParseIntError;
78    use test_case::test_case;
79
80    #[test_case(StatusCode::INTERNAL_SERVER_ERROR)]
81    #[test_case(StatusCode::SERVICE_UNAVAILABLE)]
82    #[test_case(StatusCode::REQUEST_TIMEOUT)]
83    #[test_case(StatusCode::TOO_MANY_REQUESTS)]
84    fn retryable(c: StatusCode) {
85        assert!(is_retryable_code(c));
86    }
87
88    #[test_case(StatusCode::NOT_FOUND)]
89    #[test_case(StatusCode::UNAUTHORIZED)]
90    #[test_case(StatusCode::BAD_REQUEST)]
91    #[test_case(StatusCode::BAD_GATEWAY)]
92    #[test_case(StatusCode::PRECONDITION_FAILED)]
93    fn non_retryable(c: StatusCode) {
94        assert!(!is_retryable_code(c));
95    }
96
97    #[test]
98    fn helpers() {
99        let e = super::non_retryable_from_str("test-only-err-123");
100        assert!(!e.is_transient(), "{e}");
101        let got = format!("{e}");
102        assert!(got.contains("test-only-err-123"), "{got}");
103
104        let input = "NaN".parse::<u32>().unwrap_err();
105        let e = super::non_retryable(input.clone());
106        assert!(!e.is_transient(), "{e:?}");
107        let source = e.source().and_then(|e| e.downcast_ref::<ParseIntError>());
108        assert!(matches!(source, Some(ParseIntError { .. })), "{e:?}");
109    }
110}