Skip to main content

axum_error_sets/
lib.rs

1//! Typed, composable HTTP error sets for [axum], with OpenAPI generation through
2//! [aide](https://docs.rs/aide).
3//!
4//! Instead of one large error enum per application, each function lists the exact HTTP status
5//! codes it can return, as a tuple in its return type:
6//!
7//! ```rust
8//! # use axum_error_sets::{ApiResult, codes::{NotFound, Unauthorized}};
9//! async fn get_user() -> ApiResult<String, (Unauthorized, NotFound<String>)> {
10//!     # todo!()
11//!     // ...
12//! }
13//! ```
14//!
15//! Returning a status code that isn't in the set is a compile error, and with the `aide`
16//! feature every status code in the set appears in the generated OpenAPI documentation.
17//!
18//! # Building blocks
19//! - **Status codes.** Every 4xx and 5xx status code has a wrapper type in [`codes`], such as
20//!   [`NotFound<T>`](codes::NotFound). The wrapped value `T` is the response body and must
21//!   implement [`IntoResponse`]. It defaults to `()`, which means an empty body.
22//! - **Error sets.** [`ApiResponse<S>`] is an error response whose status code is one of the
23//!   codes in the tuple `S`. [`ApiResult<T, S>`] is short for `Result<T, ApiResponse<S>>`.
24//! - **`?` conversion.** Any status code `C` converts into `ApiResponse<S>` when `S` contains
25//!   `C`, so `?` works directly. The order of the codes in the tuple doesn't matter.
26//! - **Wrapping errors.** [`ResultStatusExt`] adds methods to every `Result` for giving its
27//!   error a status code (`with_status`, `into_status`), changing it (`change_status`), or
28//!   changing the body (`map_status`, `map_status_into`).
29//! - **Growing sets.** [`ApiResultExt::into_superset`] turns a result with a small error set
30//!   into one with a larger set, so functions with narrow sets can be called from functions
31//!   with wider ones.
32//!
33//! # Example
34//! ```rust
35//! use axum::Json;
36//! use axum_error_sets::{
37//!     ApiResult, ApiResultExt as _, ResultStatusExt as _,
38//!     codes::{Internal, NotFound, Unauthorized},
39//! };
40//!
41//! fn check_token(token: &str) -> Result<(), Unauthorized> {
42//!     if token.is_empty() {
43//!         return Err(Unauthorized(()));
44//!     }
45//!     Ok(())
46//! }
47//!
48//! fn find_user(id: u32) -> ApiResult<String, (NotFound<String>,)> {
49//!     let name = lookup(id)
50//!         .ok_or("no such user")
51//!         // `&str` error -> `NotFound<String>`
52//!         .into_status::<NotFound, String>()?;
53//!     Ok(name)
54//! }
55//!
56//! async fn get_user(
57//!     token: String,
58//!     id: u32,
59//! ) -> ApiResult<Json<String>, (Unauthorized, NotFound<String>, Internal<String>)> {
60//!     // `Unauthorized` is in the set, so `?` converts it.
61//!     check_token(&token)?;
62//!
63//!     // `(NotFound<String>,)` is a subset of this handler's set.
64//!     let name = find_user(id).into_superset()?;
65//!
66//!     // A `String` error -> `Internal<String>`
67//!     let name = normalize(name).with_status::<Internal>()?;
68//!
69//!     Ok(Json(name))
70//! }
71//! # fn lookup(_: u32) -> Option<String> { None }
72//! # fn normalize(name: String) -> Result<String, String> { Ok(name) }
73//! ```
74//!
75//! Returning a status code that isn't in the set doesn't compile:
76//! ```rust,compile_fail
77//! # use axum_error_sets::{ApiResult, codes::{Forbidden, NotFound}};
78//! async fn handler() -> ApiResult<(), (NotFound,)> {
79//!     Err(Forbidden(()))?; // error: `(NotFound,): Contains<Forbidden>` is not satisfied
80//!     Ok(())
81//! }
82//! ```
83//!
84//! More examples are in the
85//! [`examples`](https://github.com/jvdwrf/axum-error-sets/tree/main/examples) directory.
86//!
87//! # Responses
88//! When an [`ApiResponse`] is returned, the response has the status code of the wrapper it was
89//! created from, even if the body's own response sets a different status. The body's
90//! [`IntoResponse`] runs as soon as the `ApiResponse` is created, not when the handler
91//! returns. Standard axum behaves differently here, which matters if `into_response` has side
92//! effects such as logging.
93//!
94//! # OpenAPI with `aide`
95//! With the `aide` feature enabled, `ApiResponse<S>` implements [`aide::OperationOutput`]
96//! whenever every body type in `S` does. Each status code in the set is then documented as a
97//! response of the operation. This works for sets of up to 16 status codes.
98//!
99//! # Typed routing
100//! [axum-typed-routing](https://docs.rs/axum-typed-routing) is a companion crate for
101//! declaring a route's path and parameters next to its handler. With its `api_route` macro,
102//! the handler's error set shows up in the generated OpenAPI documentation automatically.
103//!
104//! # Feature flags
105//! - `aide`: implements [`aide::OperationOutput`] for [`ApiResponse`].
106
107use axum::{
108    http::StatusCode,
109    response::{IntoResponse, Response},
110};
111use type_sets::{Contains, Superset};
112
113/// Short for `Result<T, ApiResponse<S>>`.
114pub type ApiResult<T, S> = Result<T, ApiResponse<S>>;
115
116/// An error response whose status code is one of the codes in the set `S`.
117///
118/// `S` is a tuple of status codes from [`codes`], for example
119/// `ApiResponse<(NotFound<String>, Internal<Json<String>>)>`. Usually it is the error type of
120/// a handler, written as `Result<T, ApiResponse<S>>` or [`ApiResult<T, S>`].
121///
122/// It implements:
123/// - [`IntoResponse`], so it can be returned from axum handlers;
124/// - `From<C>` for every status code `C` in `S`, so `?` converts status codes into it;
125/// - `aide::OperationOutput`, when the `aide` feature is enabled and every body type in `S`
126///   implements `OperationOutput`.
127///
128/// The body's [`IntoResponse`] runs when the `ApiResponse` is created, not when the handler
129/// returns. See [Responses](crate#responses).
130///
131/// # Example
132/// ```rust
133/// # use axum_error_sets::{ApiResponse, codes::*};
134/// async fn handler() -> Result<(), ApiResponse<(NotFound<String>, BadRequest<String>)>> {
135///     Err(NotFound("no such item".to_string()).into())
136/// }
137/// ```
138pub struct ApiResponse<S> {
139    response: Response,
140    code: StatusCode,
141    _marker: std::marker::PhantomData<fn() -> S>,
142}
143
144impl<S> ApiResponse<S> {
145    /// Creates an `ApiResponse` from a status code in the set `S`.
146    ///
147    /// Usually you don't need this, because `?` and [`Into::into`] do the same.
148    pub fn new<T>(wrapper: T) -> Self
149    where
150        S: Contains<T>,
151        T: StatusProvider<Inner: IntoResponse>,
152    {
153        Self::new_unchecked(wrapper.into_inner(), T::STATUS_CODE)
154    }
155
156    /// Creates an `ApiResponse` from any response and status code, without checking that the
157    /// status code is in the set `S`.
158    pub fn new_unchecked(response: impl IntoResponse, code: StatusCode) -> Self {
159        Self {
160            response: response.into_response(),
161            code,
162            _marker: std::marker::PhantomData,
163        }
164    }
165
166    /// Splits the `ApiResponse` into the body's response and the status code.
167    pub fn into_parts(self) -> (Response, StatusCode) {
168        (self.response, self.code)
169    }
170
171    /// Converts into an `ApiResponse` with a larger set `U`, which must contain every status
172    /// code in `S`. See also [`ApiResultExt::into_superset`].
173    pub fn into_superset<U>(self) -> ApiResponse<U>
174    where
175        U: Superset<S>,
176    {
177        ApiResponse {
178            response: self.response,
179            code: self.code,
180            _marker: std::marker::PhantomData,
181        }
182    }
183}
184
185impl<S> IntoResponse for ApiResponse<S> {
186    fn into_response(self) -> Response {
187        (self.code, self.response).into_response()
188    }
189}
190
191impl<S> std::fmt::Debug for ApiResponse<S> {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.debug_struct("ApiResponse")
194            .field("response", &self.response)
195            .field("code", &self.code)
196            .finish()
197    }
198}
199
200macro_rules! utoipa_aide_impls {
201    ($($n:tt => ($($E:ident),*)),+ $(,)?) => {
202        $(
203            #[allow(unused)]
204            #[cfg(feature = "aide")]
205            impl<$($E),*> aide::OperationOutput for ApiResponse<($($E,)*)>
206            where
207                $(
208                    $E: StatusProvider<Inner: aide::OperationOutput>,
209                )*
210            {
211                type Inner = (StatusCode, Response);
212
213                fn operation_response(
214                    _ctx: &mut aide::generate::GenContext,
215                    _operation: &mut aide::openapi::Operation,
216                ) -> Option<aide::openapi::Response> {
217                    None
218                }
219
220                fn inferred_responses(
221                    ctx: &mut aide::generate::GenContext,
222                    operation: &mut aide::openapi::Operation,
223                ) -> Vec<(Option<u16>, aide::openapi::Response)> {
224                    vec![
225                        $((
226                            Some($E::STATUS_CODE.as_u16()),
227                            <$E::Inner as aide::OperationOutput>
228                                ::operation_response(ctx, operation)
229                                .unwrap_or_default(),
230                        )),*
231                    ]
232                }
233            }
234        )+
235
236        // $(
237        //     #[allow(unused)]
238        //     #[cfg(feature = "utoipa")]
239        //     impl<$($E),*> utoipa::IntoResponses for ApiResponse<($($E,)*)>
240        //     where
241        //         $(
242        //             $E: StatusProvider<Inner: utoipa::ToSchema>,
243        //         )*
244        //     {
245        //         fn responses() -> std::collections::BTreeMap<
246        //             String,
247        //             utoipa::openapi::RefOr<utoipa::openapi::Response>,
248        //         > {
249        //             let mut responses = utoipa::openapi::ResponsesBuilder::new();
250
251        //             $({
252        //                 let name = < $E::Inner as utoipa::ToSchema >::name();
253        //                 let schema = < $E::Inner as utoipa::PartialSchema >::schema();
254        //                 let content = utoipa::openapi::ContentBuilder::new()
255        //                     .schema(Some(schema))
256        //                     .build();
257
258        //                 responses = responses.response(
259        //                     $E::STATUS_CODE.as_u16().to_string(),
260        //                     utoipa::openapi::response::ResponseBuilder::new()
261        //                         .description(format!(
262        //                             "{} response for {}",
263        //                             $E::STATUS_CODE.as_u16(),
264        //                             name
265        //                         ))
266        //                         .content(
267        //                             "application/json",
268        //                             content
269        //                         )
270        //                         .build()
271        //                 );
272
273        //             })*
274
275        //             responses.build().responses
276        //         }
277        //     }
278        // )+
279    };
280}
281
282utoipa_aide_impls!(
283    0 => (),
284    1 => (E1),
285    2 => (E1, E2),
286    3 => (E1, E2, E3),
287    4 => (E1, E2, E3, E4),
288    5 => (E1, E2, E3, E4, E5),
289    6 => (E1, E2, E3, E4, E5, E6),
290    7 => (E1, E2, E3, E4, E5, E6, E7),
291    8 => (E1, E2, E3, E4, E5, E6, E7, E8),
292    9 => (E1, E2, E3, E4, E5, E6, E7, E8, E9),
293    10 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10),
294    11 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11),
295    12 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12),
296    13 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13),
297    14 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14),
298    15 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15),
299    16 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15, E16),
300);
301
302/// A wrapper type that pairs a body with a fixed HTTP status code.
303///
304/// This trait is implemented for every status code in [`codes`]. The status code is part of
305/// the type, which is what lets [`ApiResponse`] track which codes a handler can return.
306pub trait StatusProvider: From<Self::Inner> + Sized {
307    /// The status code of this wrapper.
308    const STATUS_CODE: StatusCode;
309
310    /// The type of the wrapped body.
311    type Inner;
312
313    /// The same status code with a different body type, for example `NotFound<T>` for
314    /// `NotFound<String>`.
315    type WithInner<T>: StatusProvider<Inner = T>;
316
317    /// Returns the wrapped body.
318    fn into_inner(self) -> Self::Inner;
319
320    /// Converts into an [`ApiResponse`] whose set contains this status code. This is the
321    /// same as calling `into()`.
322    fn into_set<E>(self) -> ApiResponse<E>
323    where
324        Self::Inner: IntoResponse,
325        E: Contains<Self>,
326    {
327        ApiResponse::new(self)
328    }
329
330    /// Applies `f` to the body, keeping the status code.
331    fn map<T>(self, f: impl FnOnce(Self::Inner) -> T) -> Self::WithInner<T> {
332        <Self::WithInner<T> as From<T>>::from(f(self.into_inner()))
333    }
334}
335
336/// Methods on any `Result` for giving its error an HTTP status code, and for changing the
337/// status code or body of an error that already has one.
338///
339/// Each method only changes the `Err` value. `Ok` values pass through unchanged.
340pub trait ResultStatusExt<T, E>: Sized {
341    /// Wraps the error in the status code `S`. The error becomes the body.
342    ///
343    /// # Example
344    ///
345    /// ```rust
346    /// # use axum_error_sets::{ResultStatusExt as _, codes::BadRequest};
347    /// let result: Result<(), String> = Err("error".into());
348    ///
349    /// // Has type `Result<(), BadRequest<String>>`
350    /// let _wrapped = result.with_status::<BadRequest>();
351    /// ```
352    fn with_status<S>(self) -> Result<T, S::WithInner<E>>
353    where
354        S: StatusProvider<Inner = ()>;
355
356    /// Like [`with_status`](ResultStatusExt::with_status), but first converts the error into
357    /// `E2` with [`Into::into`].
358    ///
359    /// `E2` can often be inferred, as in `into_status::<BadRequest, _>()`.
360    ///
361    /// # Example
362    ///
363    /// ```rust
364    /// # use axum_error_sets::{ResultStatusExt as _, codes::BadRequest};
365    /// let result: Result<(), &str> = Err("error");
366    ///
367    /// // Has type `Result<(), BadRequest<String>>`
368    /// let _wrapped = result.into_status::<BadRequest, String>();
369    /// ```
370    fn into_status<S, E2>(self) -> Result<T, S::WithInner<E2>>
371    where
372        S: StatusProvider<Inner = ()>,
373        E: Into<E2>;
374
375    /// Replaces the error's status code with `S`, keeping the body.
376    ///
377    /// # Example
378    ///
379    /// ```rust
380    /// # use axum_error_sets::{ResultStatusExt as _, codes::{Forbidden, NotFound}};
381    /// let result: Result<(), Forbidden<String>> = Err(Forbidden("hidden".into()));
382    ///
383    /// // Has type `Result<(), NotFound<String>>`
384    /// let _changed = result.change_status::<NotFound>();
385    /// ```
386    fn change_status<S>(self) -> Result<T, S::WithInner<E::Inner>>
387    where
388        E: StatusProvider,
389        S: StatusProvider;
390
391    /// Applies `f` to the error's body, keeping the status code.
392    ///
393    /// # Example
394    ///
395    /// ```rust
396    /// # use axum::Json;
397    /// # use axum_error_sets::{ResultStatusExt as _, codes::Internal};
398    /// let result: Result<(), Internal<String>> = Err(Internal("error".into()));
399    ///
400    /// // Has type `Result<(), Internal<Json<String>>>`
401    /// let _mapped = result.map_status(Json);
402    /// ```
403    fn map_status<F, O>(self, f: F) -> Result<T, E::WithInner<O>>
404    where
405        E: StatusProvider,
406        F: FnOnce(E::Inner) -> O;
407
408    /// Converts the error's body with [`Into::into`], keeping the status code.
409    fn map_status_into<O>(self) -> Result<T, E::WithInner<O>>
410    where
411        E: StatusProvider,
412        E::Inner: Into<O>,
413    {
414        self.map_status(Into::into)
415    }
416}
417
418impl<T, E> ResultStatusExt<T, E> for Result<T, E> {
419    fn into_status<S, E2>(self) -> Result<T, S::WithInner<E2>>
420    where
421        S: StatusProvider<Inner = ()>,
422        E: Into<E2>,
423    {
424        match self {
425            Ok(val) => Ok(val),
426            Err(e) => Err(S::WithInner::from(e.into())),
427        }
428    }
429
430    fn with_status<S>(self) -> Result<T, S::WithInner<E>>
431    where
432        S: StatusProvider<Inner = ()>,
433    {
434        match self {
435            Ok(val) => Ok(val),
436            Err(e) => Err(S::WithInner::from(e)),
437        }
438    }
439
440    fn map_status<F, O>(self, f: F) -> Result<T, E::WithInner<O>>
441    where
442        E: StatusProvider,
443        F: FnOnce(E::Inner) -> O,
444    {
445        match self {
446            Ok(val) => Ok(val),
447            Err(e) => Err(e.map(f)),
448        }
449    }
450
451    fn change_status<S>(self) -> Result<T, S::WithInner<E::Inner>>
452    where
453        E: StatusProvider,
454        S: StatusProvider,
455    {
456        match self {
457            Ok(val) => Ok(val),
458            Err(e) => Err(S::WithInner::from(e.into_inner())),
459        }
460    }
461}
462
463/// Methods on `Result<T, ApiResponse<S>>`.
464pub trait ApiResultExt<T, S> {
465    /// Converts the error into an [`ApiResponse`] with a larger set `U`, which must contain
466    /// every status code in `S`.
467    ///
468    /// # Example
469    ///
470    /// ```rust
471    /// # use axum_error_sets::{ApiResult, ApiResultExt as _, codes::{Forbidden, NotFound}};
472    /// fn inner() -> ApiResult<(), (NotFound,)> {
473    ///     Ok(())
474    /// }
475    ///
476    /// fn outer() -> ApiResult<(), (Forbidden, NotFound)> {
477    ///     inner().into_superset()?;
478    ///     Ok(())
479    /// }
480    /// ```
481    fn into_superset<U>(self) -> Result<T, ApiResponse<U>>
482    where
483        U: Superset<S>;
484}
485
486impl<T, S> ApiResultExt<T, S> for Result<T, ApiResponse<S>> {
487    fn into_superset<U>(self) -> Result<T, ApiResponse<U>>
488    where
489        U: Superset<S>,
490    {
491        match self {
492            Ok(val) => Ok(val),
493            Err(e) => Err(e.into_superset()),
494        }
495    }
496}
497
498pub mod codes;