Skip to main content

nidus_http/middleware/
catch_panic.rs

1//! Panic-catching middleware that preserves the response body type.
2//!
3//! Unlike `tower_http::catch_panic`, this layer keeps the response as
4//! `Response<axum::body::Body>` so it composes with [`crate::error::ErrorEnvelopeLayer`].
5//! Place it inside the envelope (as [`crate::middleware::ApiDefaults::production`] does)
6//! so a handler panic surfaces as a structured `500` envelope with a request id and
7//! metrics, instead of aborting the connection.
8
9use std::{
10    any::Any,
11    panic::{AssertUnwindSafe, catch_unwind},
12    task::{Context, Poll},
13};
14
15use axum::{body::Body, extract::Request};
16use futures_util::{
17    FutureExt,
18    future::{CatchUnwind, Either, Map, Ready, ready},
19};
20use http::{Response, StatusCode};
21use tower::{Layer, Service};
22
23/// Creates a layer that catches panics from the inner service and maps them to a
24/// `500 Internal Server Error` response.
25///
26/// See [`CatchPanicLayer`] for details.
27pub fn catch_panic_layer() -> CatchPanicLayer {
28    CatchPanicLayer
29}
30
31/// Tower layer that catches panics from the inner service.
32///
33/// On a panic the inner service's future is abandoned, the panic payload is
34/// logged via `tracing::error!`, and a bare `500` response is returned. When
35/// layered inside [`crate::error::ErrorEnvelopeLayer`] that `500` is rendered as
36/// the production error envelope.
37#[derive(Clone, Copy, Debug, Default)]
38pub struct CatchPanicLayer;
39
40impl<S> Layer<S> for CatchPanicLayer {
41    type Service = CatchPanicService<S>;
42
43    fn layer(&self, inner: S) -> Self::Service {
44        CatchPanicService { inner }
45    }
46}
47
48/// Service produced by [`CatchPanicLayer`].
49#[derive(Clone, Debug)]
50pub struct CatchPanicService<S> {
51    inner: S,
52}
53
54type PanicPayload = Box<dyn Any + Send + 'static>;
55type CatchPanicResult<E> = Result<Result<Response<Body>, E>, PanicPayload>;
56type CatchPanicResultMapper<E> = fn(CatchPanicResult<E>) -> Result<Response<Body>, E>;
57type ImmediatePanicMapper<E> = fn(PanicPayload) -> Result<Response<Body>, E>;
58type CatchPanicFuture<F, E> = Either<
59    Map<CatchUnwind<AssertUnwindSafe<F>>, CatchPanicResultMapper<E>>,
60    Map<Ready<PanicPayload>, ImmediatePanicMapper<E>>,
61>;
62
63impl<S> Service<Request> for CatchPanicService<S>
64where
65    S: Service<Request, Response = Response<Body>> + Send + 'static,
66    S::Future: Send + 'static,
67    S::Error: Send + 'static,
68{
69    type Response = Response<Body>;
70    type Error = S::Error;
71    type Future = CatchPanicFuture<S::Future, S::Error>;
72
73    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
74        self.inner.poll_ready(cx)
75    }
76
77    fn call(&mut self, request: Request) -> Self::Future {
78        // Catch a panic that occurs synchronously while starting the inner
79        // service (e.g. inside `call`), then catch a panic that occurs while the
80        // inner future is polled.
81        match catch_unwind(AssertUnwindSafe(|| self.inner.call(request))) {
82            Ok(future) => Either::Left(
83                AssertUnwindSafe(future)
84                    .catch_unwind()
85                    .map(map_catch_panic_result::<S::Error> as CatchPanicResultMapper<S::Error>),
86            ),
87            Err(payload) => Either::Right(
88                ready(payload)
89                    .map(map_immediate_panic::<S::Error> as ImmediatePanicMapper<S::Error>),
90            ),
91        }
92    }
93}
94
95fn map_catch_panic_result<E>(result: CatchPanicResult<E>) -> Result<Response<Body>, E> {
96    match result {
97        Ok(result) => result,
98        Err(payload) => map_immediate_panic(payload),
99    }
100}
101
102fn map_immediate_panic<E>(payload: PanicPayload) -> Result<Response<Body>, E> {
103    log_panic(&payload);
104    Ok(internal_server_error())
105}
106
107fn log_panic(payload: &PanicPayload) {
108    if let Some(message) = payload.downcast_ref::<String>() {
109        tracing::error!(http.status = 500, panic.message = %message, "request handler panicked");
110    } else if let Some(message) = payload.downcast_ref::<&'static str>() {
111        tracing::error!(http.status = 500, panic.message = %message, "request handler panicked");
112    } else {
113        tracing::error!(
114            http.status = 500,
115            "request handler panicked with non-string payload"
116        );
117    }
118}
119
120fn internal_server_error() -> Response<Body> {
121    let mut response = Response::new(Body::empty());
122    *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
123    response
124}