Skip to main content

conjure_error/
error.rs

1// Copyright 2019 Palantir Technologies, Inc.
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// http://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 conjure_object::log_safety::MaybeLogSafe;
16use conjure_object::Any;
17use serde::Serialize;
18use std::borrow::Cow;
19use std::collections::hash_map::{self, HashMap};
20use std::fmt;
21use std::ops::Index;
22use std::sync::OnceLock;
23use std::time::Duration;
24use std::{backtrace, error};
25
26use crate::{ErrorType, Internal, SerializableError};
27
28/// Information about a throttle error.
29#[derive(Debug)]
30pub struct ThrottleError {
31    duration: Option<Duration>,
32}
33
34impl ThrottleError {
35    /// Returns the amount of time the client should wait before retrying, if provided.
36    #[inline]
37    pub fn duration(&self) -> Option<Duration> {
38        self.duration
39    }
40}
41
42/// Information about an unavailable error.
43#[derive(Debug)]
44pub struct UnavailableError(());
45
46/// Information about the specific type of an `Error`.
47#[derive(Debug)]
48#[non_exhaustive]
49pub enum ErrorKind {
50    /// A general service error.
51    Service(SerializableError),
52    /// A QoS error indicating that the client should throttle itself.
53    Throttle(ThrottleError),
54    /// A QoS error indicating that the server was unable to handle the request.
55    Unavailable(UnavailableError),
56}
57
58#[derive(Debug)]
59struct Inner {
60    cause: Box<dyn error::Error + Sync + Send>,
61    cause_safe: bool,
62    kind: ErrorKind,
63    safe_params: HashMap<Cow<'static, str>, Any>,
64    unsafe_params: HashMap<Cow<'static, str>, Any>,
65    backtraces: Vec<Backtrace>,
66}
67
68/// A standard error type for network services.
69///
70/// An error consists of several components:
71///
72/// * The cause of the error, represented as a type implementing the Rust `Error` trait. The cause can either be
73///   declared safe or unsafe to log.
74/// * The error's kind, indicating how the service should handle the error e.g. in a response to a client.
75/// * Backtraces, including one taken at the time the error was created.
76/// * Parameters adding extra context about the error. They can be declared either safe or unsafe to log.
77///
78/// Note that this type does *not* implement the standard library's `Error` trait.
79#[derive(Debug)]
80pub struct Error(Box<Inner>);
81
82impl Error {
83    /// Creates a service error with an unsafe cause.
84    pub fn service<E, T>(cause: E, error_type: T) -> Error
85    where
86        E: Into<Box<dyn error::Error + Sync + Send>>,
87        T: ErrorType + Serialize,
88    {
89        Error::service_inner(
90            cause.into(),
91            false,
92            crate::encode(&error_type),
93            T::safe_args(),
94        )
95    }
96
97    /// Creates a service error with a safe cause.
98    pub fn service_safe<E, T>(cause: E, error_type: T) -> Error
99    where
100        E: Into<Box<dyn error::Error + Sync + Send>>,
101        T: ErrorType + Serialize,
102    {
103        Error::service_inner(
104            cause.into(),
105            true,
106            crate::encode(&error_type),
107            T::safe_args(),
108        )
109    }
110
111    /// Creates a service error from a propagated error description and an unsafe cause.
112    pub fn propagated_service<E>(cause: E, error: SerializableError) -> Error
113    where
114        E: Into<Box<dyn error::Error + Sync + Send>>,
115    {
116        Error::service_inner(cause.into(), false, error, &[])
117    }
118
119    /// Creates a service error from a propagated error description and a safe cause.
120    pub fn propagated_service_safe<E>(cause: E, error: SerializableError) -> Error
121    where
122        E: Into<Box<dyn error::Error + Sync + Send>>,
123    {
124        Error::service_inner(cause.into(), true, error, &[])
125    }
126
127    fn service_inner(
128        cause: Box<dyn error::Error + Sync + Send>,
129        cause_safe: bool,
130        error: SerializableError,
131        safe_args: &[&str],
132    ) -> Error {
133        let mut safe_params = HashMap::new();
134        let mut unsafe_params = HashMap::new();
135
136        for (key, value) in error.parameters() {
137            let key = Cow::Owned(key.clone());
138            let value = Any::new(value).unwrap();
139            if safe_args.contains(&&*key) {
140                safe_params.insert(key, value);
141            } else {
142                unsafe_params.insert(key, value);
143            }
144        }
145
146        let mut error = Error::new(cause, cause_safe, ErrorKind::Service(error));
147        error.0.safe_params = safe_params;
148        error.0.unsafe_params = unsafe_params;
149        error
150    }
151
152    /// Creates an error indicating that the client should throttle itself with an unsafe cause.
153    pub fn throttle<E>(cause: E) -> Error
154    where
155        E: Into<Box<dyn error::Error + Sync + Send>>,
156    {
157        Error::new(
158            cause.into(),
159            false,
160            ErrorKind::Throttle(ThrottleError { duration: None }),
161        )
162    }
163
164    /// Creates an error indicating that the client should throttle itself with a safe cause.
165    pub fn throttle_safe<E>(cause: E) -> Error
166    where
167        E: Into<Box<dyn error::Error + Sync + Send>>,
168    {
169        Error::new(
170            cause.into(),
171            true,
172            ErrorKind::Throttle(ThrottleError { duration: None }),
173        )
174    }
175
176    /// Creates an error indicating that the client should throttle itself for a specific duration with an unsafe
177    /// cause.
178    pub fn throttle_for<E>(cause: E, duration: Duration) -> Error
179    where
180        E: Into<Box<dyn error::Error + Sync + Send>>,
181    {
182        Error::new(
183            cause.into(),
184            false,
185            ErrorKind::Throttle(ThrottleError {
186                duration: Some(duration),
187            }),
188        )
189    }
190
191    /// Creates an error indicating that the client should throttle itself for a specific duration with a safe
192    /// cause.
193    pub fn throttle_for_safe<E>(cause: E, duration: Duration) -> Error
194    where
195        E: Into<Box<dyn error::Error + Sync + Send>>,
196    {
197        Error::new(
198            cause.into(),
199            true,
200            ErrorKind::Throttle(ThrottleError {
201                duration: Some(duration),
202            }),
203        )
204    }
205
206    /// Creates an error indicating that the server was unable to serve the client's request with an unsafe cause.
207    pub fn unavailable<E>(cause: E) -> Error
208    where
209        E: Into<Box<dyn error::Error + Sync + Send>>,
210    {
211        Error::new(
212            cause.into(),
213            false,
214            ErrorKind::Unavailable(UnavailableError(())),
215        )
216    }
217
218    /// Creates an error indicating that the server was unable to serve the client's request with a safe cause.
219    pub fn unavailable_safe<E>(cause: E) -> Error
220    where
221        E: Into<Box<dyn error::Error + Sync + Send>>,
222    {
223        Error::new(
224            cause.into(),
225            true,
226            ErrorKind::Unavailable(UnavailableError(())),
227        )
228    }
229
230    /// A convenience function to construct an internal service error with an unsafe cause.
231    pub fn internal<E>(cause: E) -> Error
232    where
233        E: Into<Box<dyn error::Error + Sync + Send>>,
234    {
235        Error::service(cause, Internal::new())
236    }
237
238    /// A convenience function to construct an internal service error with a safe cause.
239    pub fn internal_safe<E>(cause: E) -> Error
240    where
241        E: Into<Box<dyn error::Error + Sync + Send>>,
242    {
243        Error::service_safe(cause, Internal::new())
244    }
245
246    fn new(cause: Box<dyn error::Error + Sync + Send>, cause_safe: bool, kind: ErrorKind) -> Error {
247        let inner = Inner {
248            cause,
249            cause_safe,
250            kind,
251            safe_params: HashMap::new(),
252            unsafe_params: HashMap::new(),
253            backtraces: vec![],
254        };
255        Error(Box::new(inner)).with_backtrace()
256    }
257
258    /// Returns the error's cause.
259    ///
260    /// Use the `cause_safe` method to determine if the error is safe or not.
261    #[inline]
262    pub fn cause(&self) -> &(dyn error::Error + 'static + Sync + Send) {
263        &*self.0.cause
264    }
265
266    /// Returns whether or not the error's cause is considered safe.
267    #[inline]
268    pub fn cause_safe(&self) -> bool {
269        self.0.cause_safe
270    }
271
272    /// Returns kind-specific error information.
273    #[inline]
274    pub fn kind(&self) -> &ErrorKind {
275        &self.0.kind
276    }
277
278    /// Adds a new safe parameter to the error.
279    ///
280    /// The `MaybeLogSafe` bound is gated on the `log-safety` feature.
281    /// When enabled it requires `T: LogSafe`.
282    /// When disabled it accepts any `T`.
283    /// To log a non-`LogSafe` value, wrap it in [`AssertLogSafe`].
284    ///
285    /// # Panics
286    ///
287    /// Panics if the value fails to serialize.
288    pub fn with_safe_param<T>(mut self, key: &'static str, value: T) -> Error
289    where
290        T: Serialize + MaybeLogSafe,
291    {
292        let value = Any::new(value).expect("value failed to serialize");
293        self.0.safe_params.insert(Cow::Borrowed(key), value);
294        self
295    }
296
297    /// Adds a new unsafe parameter to the error.
298    ///
299    /// # Panics
300    ///
301    /// Panics if the value fails to serialize.
302    pub fn with_unsafe_param<T>(mut self, key: &'static str, value: T) -> Error
303    where
304        T: Serialize,
305    {
306        let value = Any::new(value).expect("value failed to serialize");
307        self.0.unsafe_params.insert(Cow::Borrowed(key), value);
308        self
309    }
310
311    /// Returns the error's safe parameters.
312    #[inline]
313    pub fn safe_params(&self) -> Params<'_> {
314        Params(&self.0.safe_params)
315    }
316
317    /// Returns the error's unsafe parameters.
318    #[inline]
319    pub fn unsafe_params(&self) -> Params<'_> {
320        Params(&self.0.unsafe_params)
321    }
322
323    /// Adds a new backtrace to the error.
324    ///
325    /// An error always takes a backtrace at the time of its construction, but this method can be used to add extra
326    /// backtraces to it. For example, this might be used when transferring an error from one thread to another.
327    #[inline]
328    pub fn with_backtrace(mut self) -> Error {
329        self.0.backtraces.push(Backtrace::new());
330        self
331    }
332
333    /// Adds a new custom backtrace to the error which is safe to log
334    #[inline]
335    pub fn with_custom_safe_backtrace(mut self, backtrace: String) -> Error {
336        self.0.backtraces.push(Backtrace::custom(backtrace));
337        self
338    }
339
340    /// Returns the error's backtraces, ordered from oldest to newest.
341    #[inline]
342    pub fn backtraces(&self) -> &[Backtrace] {
343        &self.0.backtraces
344    }
345}
346
347/// A collection of error parameters, either safe or unsafe.
348#[derive(Debug)]
349pub struct Params<'a>(&'a HashMap<Cow<'static, str>, Any>);
350
351impl<'a> Params<'a> {
352    /// Returns an iterator over the key-value parameter pairs.
353    #[inline]
354    pub fn iter(&self) -> ParamsIter<'a> {
355        ParamsIter(self.0.iter())
356    }
357
358    /// Returns the number of parameters.
359    #[inline]
360    pub fn len(&self) -> usize {
361        self.0.len()
362    }
363
364    /// Determines if there are no parameters.
365    #[inline]
366    pub fn is_empty(&self) -> bool {
367        self.0.is_empty()
368    }
369}
370
371impl Index<&str> for Params<'_> {
372    type Output = Any;
373
374    #[inline]
375    fn index(&self, key: &str) -> &Any {
376        &self.0[key]
377    }
378}
379
380impl<'a> IntoIterator for &Params<'a> {
381    type Item = (&'a str, &'a Any);
382    type IntoIter = ParamsIter<'a>;
383
384    #[inline]
385    fn into_iter(self) -> ParamsIter<'a> {
386        self.iter()
387    }
388}
389
390/// An iterator over the parameters of an error.
391pub struct ParamsIter<'a>(hash_map::Iter<'a, Cow<'static, str>, Any>);
392
393impl<'a> Iterator for ParamsIter<'a> {
394    type Item = (&'a str, &'a Any);
395
396    #[inline]
397    fn next(&mut self) -> Option<(&'a str, &'a Any)> {
398        self.0.next().map(|(a, b)| (&**a, b))
399    }
400
401    #[inline]
402    fn size_hint(&self) -> (usize, Option<usize>) {
403        self.0.size_hint()
404    }
405}
406
407static BACKTRACE_PROVIDER: OnceLock<Box<dyn Fn() -> String + Send + Sync>> = OnceLock::new();
408
409/// Overrides backtrace capture. The returned string lands verbatim in the
410/// `stacktrace` field of service.1 records and is treated as safe-to-log. Used for platforms
411/// like wasm with that backtrace::Backtrace doesn't support.
412pub fn set_safe_custom_backtrace_provider<F>(provider: F) -> Result<(), Error>
413where
414    F: Fn() -> String + Send + Sync + 'static,
415{
416    BACKTRACE_PROVIDER
417        .set(Box::new(provider))
418        .or(Err(Error::internal_safe("backtrace provider already set")))
419}
420
421/// A backtrace associated with an `Error`.
422pub struct Backtrace(BacktraceInner);
423
424impl fmt::Debug for Backtrace {
425    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
426        match &self.0 {
427            BacktraceInner::Rust(b) => fmt::Display::fmt(b, fmt),
428            BacktraceInner::Custom(b) => fmt::Display::fmt(b, fmt),
429        }
430    }
431}
432
433impl Backtrace {
434    #[inline]
435    fn new() -> Backtrace {
436        if let Some(provider) = BACKTRACE_PROVIDER.get() {
437            Backtrace(BacktraceInner::Custom(provider()))
438        } else {
439            Backtrace(BacktraceInner::Rust(backtrace::Backtrace::force_capture()))
440        }
441    }
442
443    #[inline]
444    fn custom(s: String) -> Backtrace {
445        Backtrace(BacktraceInner::Custom(s))
446    }
447}
448
449enum BacktraceInner {
450    Rust(backtrace::Backtrace),
451    Custom(String),
452}