google_cloud_gax/error/
credentials.rs

1// Copyright 2025 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
15use std::error::Error;
16use std::fmt::{Debug, Display, Formatter, Result};
17use std::sync::Arc;
18
19type ArcError = Arc<dyn Error + Send + Sync>;
20
21/// Represents an error using [Credentials].
22///
23/// The Google Cloud client libraries may experience problems using credentials
24/// to create the necessary authentication headers. For example, a temporary
25/// failure to retrieve or create [access tokens]. Note that these failures may
26/// happen even after the credentials files are successfully loaded and parsed.
27///
28/// Applications rarely need to create instances of this error type. The
29/// exception might be when testing application code, where the application is
30/// mocking a client library behavior. Such tests are extremely rare, most
31/// applications should only work with the [Error][crate::error::Error] type.
32///
33/// # Example
34/// ```
35/// use google_cloud_gax::error::CredentialsError;
36/// let mut headers = fetch_headers();
37/// while let Err(e) = &headers {
38///     if e.is_transient() {
39///         headers = fetch_headers();
40///     }
41/// }
42///
43/// fn fetch_headers() -> Result<http::HeaderMap, CredentialsError> {
44///   # Ok(http::HeaderMap::new())
45/// }
46/// ```
47///
48/// [access tokens]: https://cloud.google.com/docs/authentication/token-types
49/// [Credentials]: https://docs.rs/google-cloud-auth/latest/google_cloud_auth/credentials/struct.Credential.html
50#[derive(Clone, Debug)]
51pub struct CredentialsError {
52    is_transient: bool,
53    message: Option<String>,
54    source: Option<ArcError>,
55}
56
57impl CredentialsError {
58    /// Creates a new `CredentialsError`.
59    ///
60    /// This function is only intended for use in the client libraries
61    /// implementation. Application may use this in mocks, though we do not
62    /// recommend that you write tests for specific error cases. Most tests
63    /// should use the generic [Error][crate::error::Error] type.
64    ///
65    /// # Example
66    /// ```
67    /// use google_cloud_gax::error::CredentialsError;
68    /// let mut headers = fetch_headers();
69    /// while let Err(e) = &headers {
70    ///     if e.is_transient() {
71    ///         headers = fetch_headers();
72    ///     }
73    /// }
74    ///
75    /// fn fetch_headers() -> Result<http::HeaderMap, CredentialsError> {
76    ///   # Ok(http::HeaderMap::new())
77    /// }
78    /// ```
79    ///
80    /// # Parameters
81    /// * `is_transient` - if true, the operation may succeed in future attempts.
82    /// * `source` - The underlying error that caused the auth failure.
83    #[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
84    pub fn from_source<T: Error + Send + Sync + 'static>(is_transient: bool, source: T) -> Self {
85        CredentialsError {
86            is_transient,
87            source: Some(Arc::new(source)),
88            message: None,
89        }
90    }
91
92    /// Creates a new `CredentialsError`.
93    ///
94    /// This function is only intended for use in the client libraries
95    /// implementation. Application may use this in mocks, though we do not
96    /// recommend that you write tests for specific error cases. Most tests
97    /// should use the generic [Error][crate::error::Error] type.
98    ///
99    /// # Example
100    /// ```
101    /// # use google_cloud_gax::error::CredentialsError;
102    /// let err = CredentialsError::from_msg(
103    ///     true, "simulated retryable error while trying to create credentials");
104    /// assert!(err.is_transient());
105    /// assert!(format!("{err}").contains("simulated retryable error"));
106    /// ```
107    ///
108    /// # Parameters
109    /// * `is_transient` - if true, the operation may succeed in future attempts.
110    /// * `message` - The underlying error that caused the auth failure.
111    #[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
112    pub fn from_msg<T: Into<String>>(is_transient: bool, message: T) -> Self {
113        CredentialsError {
114            is_transient,
115            message: Some(message.into()),
116            source: None,
117        }
118    }
119
120    /// Creates a new `CredentialsError`.
121    ///
122    /// This function is only intended for use in the client libraries
123    /// implementation. Application may use this in mocks, though we do not
124    /// recommend that you write tests for specific error cases. Most tests
125    /// should use the generic [Error][crate::error::Error] type.
126    ///
127    /// # Example
128    /// ```
129    /// # use google_cloud_gax::error::CredentialsError;
130    /// let source = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "cannot connect");
131    /// let err = CredentialsError::new(
132    ///     true,
133    ///     "simulated retryable error while trying to create credentials",
134    ///     source);
135    /// assert!(err.is_transient());
136    /// assert!(format!("{err}").contains("simulated retryable error"));
137    /// ```
138    ///
139    /// # Parameters
140    /// * `is_transient` - if true, the operation may succeed in future attempts.
141    /// * `message` - The underlying error that caused the auth failure.
142    #[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
143    pub fn new<M, S>(is_transient: bool, message: M, source: S) -> Self
144    where
145        M: Into<String>,
146        S: std::error::Error + Send + Sync + 'static,
147    {
148        CredentialsError {
149            is_transient,
150            message: Some(message.into()),
151            source: Some(Arc::new(source)),
152        }
153    }
154
155    /// Returns true if the error is transient and may succeed in future attempts.
156    ///
157    /// # Example
158    /// ```
159    /// # use google_cloud_gax::error::CredentialsError;
160    /// let mut headers = fetch_headers();
161    /// while let Err(e) = &headers {
162    ///     if e.is_transient() {
163    ///         headers = fetch_headers();
164    ///     }
165    /// }
166    ///
167    /// fn fetch_headers() -> Result<http::HeaderMap, CredentialsError> {
168    ///   # Ok(http::HeaderMap::new())
169    /// }
170    /// ```
171    pub fn is_transient(&self) -> bool {
172        self.is_transient
173    }
174}
175
176impl std::error::Error for CredentialsError {
177    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
178        self.source
179            .as_ref()
180            .map(|arc| arc.as_ref() as &(dyn std::error::Error + 'static))
181    }
182}
183
184const TRANSIENT_MSG: &str = "but future attempts may succeed";
185const PERMANENT_MSG: &str = "and future attempts will not succeed";
186
187impl Display for CredentialsError {
188    /// Formats the error message to include retryability and source.
189    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
190        let msg = if self.is_transient {
191            TRANSIENT_MSG
192        } else {
193            PERMANENT_MSG
194        };
195        match &self.message {
196            None => write!(f, "cannot create auth headers {msg}"),
197            Some(m) => write!(f, "{m} {msg}"),
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use test_case::test_case;
206
207    #[test_case(true)]
208    #[test_case(false)]
209    fn from_source(transient: bool) {
210        let source = wkt::TimestampError::OutOfRange;
211        let got = CredentialsError::from_source(transient, source);
212        assert_eq!(got.is_transient(), transient, "{got:?}");
213        assert!(
214            got.source()
215                .and_then(|e| e.downcast_ref::<wkt::TimestampError>())
216                .is_some(),
217            "{got:?}"
218        );
219        assert!(
220            got.to_string().contains("cannot create auth headers"),
221            "{got:?}"
222        );
223    }
224
225    #[test_case(true)]
226    #[test_case(false)]
227    fn from_str(transient: bool) {
228        let got = CredentialsError::from_msg(transient, "test-only");
229        assert_eq!(got.is_transient(), transient, "{got:?}");
230        assert!(got.source().is_none(), "{got:?}");
231        assert!(got.to_string().contains("test-only"), "{got}");
232    }
233
234    #[test_case(true)]
235    #[test_case(false)]
236    fn new(transient: bool) {
237        let source = wkt::TimestampError::OutOfRange;
238        let got = CredentialsError::new(transient, "additional information", source);
239        assert_eq!(got.is_transient(), transient, "{got:?}");
240        assert!(
241            got.source()
242                .and_then(|e| e.downcast_ref::<wkt::TimestampError>())
243                .is_some(),
244            "{got:?}"
245        );
246        assert!(
247            got.to_string().contains("additional information"),
248            "{got:?}"
249        );
250    }
251
252    #[test]
253    fn fmt() {
254        let e = CredentialsError::from_msg(true, "test-only-err-123");
255        let got = format!("{e}");
256        assert!(got.contains("test-only-err-123"), "{got}");
257        assert!(got.contains(TRANSIENT_MSG), "{got}");
258
259        let e = CredentialsError::from_msg(false, "test-only-err-123");
260        let got = format!("{e}");
261        assert!(got.contains("test-only-err-123"), "{got}");
262        assert!(got.contains(PERMANENT_MSG), "{got}");
263    }
264}