conjure_error/
lib.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
15//! Runtime support for Conjure error types.
16//!
17//! In a networked service, the error objects that are propagated through its codebase are responsible for two things:
18//!
19//! * Collecting useful information that a developer can use to diagnose whatever problem caused the error.
20//! * Controlling how the error is presented to the client.
21//!
22//! Services implemented using Conjure's frameworks use the [`Error`] type defined in this crate as the single error
23//! type throughout the codebase. [`Error`]s store:
24//!
25//! * Developer facing:
26//!   * [`Error::cause`] - the underlying cause of the error. This can be a type implementing the Rust
27//!     [`std::error::Error`] trait, or just a [`str`] or [`String`] containing a description of what happened. When
28//!     an [`Error`] is logged, the cause (and its chain of sources via [`std::error::Error::source`]) are included as a
29//!     parameter. The log-safety of that cause information is identified by the choice of constructor of the [`Error`].
30//!   * [`Error::safe_params`] and [`Error::unsafe_params`] - key-value pairs that can be added to the error to provide
31//!     context. When an [`Error`] is logged, these are included in the service log's parameters. When a service
32//!     [`Error`] is created, all of the parameters of its associated Conjure error are automatically included as
33//!     params, with [`ErrorType::safe_args`] used to partition the parameters between safe and unsafe. Additional
34//!     params can be added via [`Error::with_safe_param`] and [`Error::with_unsafe_param`].
35//!   * [`Error::backtraces`] - a sequence of backtraces to annotate the error with the state of the function call
36//!     stack. A backtrace is automatically taken when the [`Error`] is created, and additional backtraces can be added
37//!     with the [`Error::with_backtrace`] method. This can be used when, for example, an [`Error`] transfers from one
38//!     thread to another. When an [`Error`] is logged, its backtraces will be included in the stacktrace field.
39//! * Client facing:
40//!   * [`Error::kind`] - how the error should be reported to the client. There are currently three kinds:
41//!       * [`ErrorKind::Service`] - a standard service error. These are constructed from a type implementing
42//!         [`Serialize`] and [`ErrorType`]. The value is expected to serialize as a struct, with the struct's fields
43//!         being the parameters of the error. Errors defined in Conjure APIs will generate types implementing these
44//!         traits. This will generate an HTTP response following the [Conjure wire spec]. Service errors are created
45//!         with the [`Error::service`], [`Error::service_safe`], [`Error::internal`], and [`Error::internal_safe`]
46//!         functions.
47//!       * [`ErrorKind::Throttle`] - an indication that the client is making too many requests and should throttle
48//!         itself. This will generate a `429 Too Many Requests` HTTP response. Throttle errors are created with the
49//!         [`Error::throttle`], [`Error::throttle_safe`], [`Error::throttle_for`], and [`Error::throttle_for_safe`]
50//!         functions.
51//!       * [`ErrorKind::Unavailable`] - an indication that the server is unable to handle the request. This will
52//!         generate a `503 Service Unavailable` HTTP response. Unavailable errors are created with the
53//!         [`Error::unavailable`] and [`Error::unavailable_safe`] functions.
54//!
55//! [Conjure wire spec]: https://github.com/palantir/conjure/blob/master/docs/spec/wire.md#34-conjure-errors
56//!
57//! ## Examples
58//!
59//! Mapping a [`std::error::Error`] returned by a stdlib API into a generic internal service error:
60//!
61//! ```rust,no_run
62//! use conjure_error::Error;
63//! use std::fs::File;
64//!
65//! # fn foo() -> Result<(), Error> {
66//! let file = File::open("var/data/database.csv").map_err(Error::internal_safe)?;
67//! # Ok(()) }
68//! ```
69//!
70//! Doing the same, but including the filename as an extra parameter:
71//!
72//! ```rust,no_run
73//! use conjure_error::Error;
74//! use std::fs::File;
75//!
76//! # fn foo() -> Result<(), Error> {
77//! let filename = "var/data/database.csv";
78//! let file = File::open(filename).map_err(|e| {
79//!     Error::internal_safe(e).with_safe_param("filename", filename)
80//! })?;
81//! # Ok(()) }
82//! ```
83//!
84//! Returning a specific Conjure error when there is no existing error cause:
85//!
86//! ```yaml
87//! types:
88//!   definitions:
89//!     errors:
90//!       ObjectNotFound:
91//!         namespace: MyService
92//!         code: INVALID_ARGUMENT
93//!         safe-args:
94//!           objectRid: rid
95//! ```
96//!
97//! ```rust,ignore
98//! use conjure_error::Error;
99//! use my_service_api::errors::ObjectNotFound;
100//!
101//! if !object_was_found {
102//!     return Err(Error::service_safe("failed to find object", ObjectNotFound::new(object_rid)));
103//! }
104//! ```
105#![warn(clippy::all, missing_docs)]
106
107extern crate self as conjure_error;
108
109use conjure_object::Uuid;
110use serde::{Serialize, Serializer};
111
112use crate::ser::{ParametersSerializer, StringSeed};
113
114pub use crate::custom_types::*;
115pub use crate::error::*;
116pub use crate::types::errors::*;
117pub use crate::types::objects::*;
118use serde::de::DeserializeSeed;
119
120mod error;
121mod ser;
122#[allow(clippy::all, missing_docs)]
123#[rustfmt::skip]
124mod types;
125#[allow(clippy::all, missing_docs)]
126#[rustfmt::skip]
127mod custom_types;
128
129impl ErrorCode {
130    /// Returns the HTTP status code associated with the error code.
131    #[inline]
132    pub fn status_code(&self) -> u16 {
133        match self {
134            ErrorCode::PermissionDenied => 403,
135            ErrorCode::InvalidArgument => 400,
136            ErrorCode::NotFound => 404,
137            ErrorCode::Conflict => 409,
138            ErrorCode::RequestEntityTooLarge => 413,
139            ErrorCode::FailedPrecondition => 500,
140            ErrorCode::Internal => 500,
141            ErrorCode::Timeout => 500,
142            ErrorCode::CustomClient => 400,
143            ErrorCode::CustomServer => 500,
144        }
145    }
146}
147
148/// A trait implemented by Conjure error types.
149pub trait ErrorType {
150    /// Returns the error's code.
151    fn code() -> ErrorCode;
152
153    /// Returns the error's name.
154    ///
155    /// The name must be formatted like `NamespaceName:ErrorName`.
156    fn name() -> &'static str;
157
158    /// Returns a sorted slice of the names of the error's safe parameters.
159    fn safe_args() -> &'static [&'static str];
160
161    /// Returns the error's instance ID, if it stores one.
162    ///
163    /// The default implementation returns `None`.
164    #[inline]
165    fn instance_id(&self) -> Option<Uuid> {
166        None
167    }
168
169    /// Wraps the error in another that overrides its instance ID.
170    #[inline]
171    fn with_instance_id(self, instance_id: Uuid) -> WithInstanceId<Self>
172    where
173        Self: Sized,
174    {
175        WithInstanceId {
176            error: self,
177            instance_id,
178        }
179    }
180}
181
182impl<T> ErrorType for &T
183where
184    T: ?Sized + ErrorType,
185{
186    #[inline]
187    fn code() -> ErrorCode {
188        T::code()
189    }
190
191    #[inline]
192    fn name() -> &'static str {
193        T::name()
194    }
195
196    #[inline]
197    fn instance_id(&self) -> Option<Uuid> {
198        (**self).instance_id()
199    }
200
201    #[inline]
202    fn safe_args() -> &'static [&'static str] {
203        T::safe_args()
204    }
205}
206
207/// An `ErrorType` which wraps another and overrides its instance ID.
208pub struct WithInstanceId<T> {
209    error: T,
210    instance_id: Uuid,
211}
212
213impl<T> ErrorType for WithInstanceId<T>
214where
215    T: ErrorType,
216{
217    fn code() -> ErrorCode {
218        T::code()
219    }
220
221    fn name() -> &'static str {
222        T::name()
223    }
224
225    fn safe_args() -> &'static [&'static str] {
226        T::safe_args()
227    }
228
229    fn instance_id(&self) -> Option<Uuid> {
230        Some(self.instance_id)
231    }
232}
233
234impl<T> Serialize for WithInstanceId<T>
235where
236    T: Serialize,
237{
238    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
239    where
240        S: Serializer,
241    {
242        self.error.serialize(s)
243    }
244}
245
246/// Encodes a Conjure error into its serialized form.
247///
248/// The error's instance ID will be randomly generated.
249///
250/// # Panics
251///
252/// Panics if the error type does not serialize as a struct.
253pub fn encode<T>(error: &T) -> SerializableError
254where
255    T: ErrorType + Serialize,
256{
257    let parameters = error
258        .serialize(ParametersSerializer)
259        .expect("failed to serialize error parameters");
260
261    SerializableError::builder()
262        .error_code(T::code())
263        .error_name(T::name())
264        .error_instance_id(error.instance_id().unwrap_or_else(Uuid::new_v4))
265        .parameters(parameters)
266        .build()
267}
268
269/// Re-serializes the parameters of a [`SerializableError`] in the legacy stringified format.
270///
271/// Scalar parameters will be converted to their string representations and composite parameters
272/// will be dropped.
273pub fn stringify_parameters(error: SerializableError) -> SerializableError {
274    let mut stringified_parameters = vec![];
275
276    for (key, value) in error.parameters() {
277        if let Ok(value) = StringSeed.deserialize(value.clone()) {
278            stringified_parameters.push((key.clone(), value));
279        }
280    }
281
282    serializable_error::Builder::from(error)
283        .parameters(stringified_parameters)
284        .build()
285}