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}