Skip to main content

rama_http/layer/
catch_panic.rs

1//! Convert panics into responses.
2//!
3//! Note that using panics for error handling is _not_ recommended. Prefer instead to use `Result`
4//! whenever possible.
5//!
6//! # Example
7//!
8//! ```rust
9//! use std::convert::Infallible;
10//!
11//! use rama_http::{Request, Response, Body, header::HeaderName};
12//! use rama_http::layer::catch_panic::CatchPanicLayer;
13//! use rama_core::service::service_fn;
14//! use rama_core::{Service, Layer};
15//! use rama_core::error::BoxError;
16//!
17//! # #[tokio::main]
18//! # async fn main() -> Result<(), BoxError> {
19//! async fn handle(req: Request) -> Result<Response, Infallible> {
20//!     panic!("something went wrong...")
21//! }
22//!
23//! let mut svc = (
24//!     // Catch panics and convert them into responses.
25//!     CatchPanicLayer::new(),
26//! ).into_layer(service_fn(handle));
27//!
28//! // Call the service.
29//! let request = Request::new(Body::default());
30//!
31//! let response = svc.serve(request).await?;
32//!
33//! assert_eq!(response.status(), 500);
34//! #
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! Using a custom panic handler:
40//!
41//! ```rust
42//! use std::{any::Any, convert::Infallible};
43//!
44//! use rama_http::{Body, Request, StatusCode, Response, header::{self, HeaderName}};
45//! use rama_http::layer::catch_panic::CatchPanicLayer;
46//! use rama_core::service::{Service, service_fn};
47//! use rama_core::Layer;
48//! use rama_core::error::BoxError;
49//!
50//! # #[tokio::main]
51//! # async fn main() -> Result<(), BoxError> {
52//! async fn handle(req: Request) -> Result<Response, Infallible> {
53//!     panic!("something went wrong...")
54//! }
55//!
56//! fn handle_panic(err: Box<dyn Any + Send + 'static>) -> Response {
57//!     let details = if let Some(s) = err.downcast_ref::<String>() {
58//!         s.clone()
59//!     } else if let Some(s) = err.downcast_ref::<&str>() {
60//!         s.to_string()
61//!     } else {
62//!         "Unknown panic message".to_string()
63//!     };
64//!
65//!     let body = serde_json::json!({
66//!         "error": {
67//!             "kind": "panic",
68//!             "details": details,
69//!         }
70//!     });
71//!     let body = serde_json::to_string(&body).unwrap();
72//!
73//!     Response::builder()
74//!         .status(StatusCode::INTERNAL_SERVER_ERROR)
75//!         .header(header::CONTENT_TYPE, "application/json")
76//!         .body(Body::from(body))
77//!         .unwrap()
78//! }
79//!
80//! let svc = (
81//!     // Use `handle_panic` to create the response.
82//!     CatchPanicLayer::custom(handle_panic),
83//! ).into_layer(service_fn(handle));
84//! #
85//! # Ok(())
86//! # }
87//! ```
88
89#![expect(
90    clippy::allow_attributes,
91    reason = "macro-generated `#[allow]` attributes whose underlying lints fire only for some expansions"
92)]
93
94use crate::{Body, HeaderValue, Request, Response, StatusCode};
95use rama_core::futures::FutureExt;
96use rama_core::telemetry::tracing;
97use rama_core::{Layer, Service};
98use rama_utils::macros::define_inner_service_accessors;
99use std::{any::Any, panic::AssertUnwindSafe};
100
101/// Layer that applies the [`CatchPanic`] middleware that catches panics and converts them into
102/// `500 Internal Server` responses.
103///
104/// See the [module docs](self) for an example.
105#[derive(Debug, Clone)]
106pub struct CatchPanicLayer<T> {
107    panic_handler: T,
108}
109
110impl Default for CatchPanicLayer<DefaultResponseForPanic> {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116impl CatchPanicLayer<DefaultResponseForPanic> {
117    /// Create a new `CatchPanicLayer` with the [`Default`]] panic handler.
118    #[must_use]
119    pub const fn new() -> Self {
120        Self {
121            panic_handler: DefaultResponseForPanic,
122        }
123    }
124}
125
126impl<T> CatchPanicLayer<T> {
127    /// Create a new `CatchPanicLayer` with a custom panic handler.
128    pub fn custom(panic_handler: T) -> Self
129    where
130        T: ResponseForPanic,
131    {
132        Self { panic_handler }
133    }
134}
135
136impl<T, S> Layer<S> for CatchPanicLayer<T>
137where
138    T: Clone,
139{
140    type Service = CatchPanic<S, T>;
141
142    fn layer(&self, inner: S) -> Self::Service {
143        CatchPanic {
144            inner,
145            panic_handler: self.panic_handler.clone(),
146        }
147    }
148
149    fn into_layer(self, inner: S) -> Self::Service {
150        CatchPanic {
151            inner,
152            panic_handler: self.panic_handler,
153        }
154    }
155}
156
157/// Middleware that catches panics and converts them into `500 Internal Server` responses.
158///
159/// See the [module docs](self) for an example.
160#[derive(Debug, Clone)]
161pub struct CatchPanic<S, T> {
162    inner: S,
163    panic_handler: T,
164}
165
166impl<S> CatchPanic<S, DefaultResponseForPanic> {
167    /// Create a new `CatchPanic` with the default panic handler.
168    pub const fn new(inner: S) -> Self {
169        Self {
170            inner,
171            panic_handler: DefaultResponseForPanic,
172        }
173    }
174}
175
176impl<S, T> CatchPanic<S, T> {
177    define_inner_service_accessors!();
178
179    /// Create a new `CatchPanic` with a custom panic handler.
180    pub const fn custom(inner: S, panic_handler: T) -> Self
181    where
182        T: ResponseForPanic,
183    {
184        Self {
185            inner,
186            panic_handler,
187        }
188    }
189}
190
191impl<S, T, ReqBody, ResBody> Service<Request<ReqBody>> for CatchPanic<S, T>
192where
193    S: Service<Request<ReqBody>, Output = Response<ResBody>>,
194    ResBody: Into<Body> + Send + 'static,
195    T: ResponseForPanic + Clone + Send + Sync + 'static,
196    ReqBody: Send + 'static,
197    ResBody: Send + 'static,
198{
199    type Output = Response;
200    type Error = S::Error;
201
202    async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
203        let future = match std::panic::catch_unwind(AssertUnwindSafe(|| self.inner.serve(req))) {
204            Ok(future) => future,
205            Err(panic_err) => return Ok(self.panic_handler.response_for_panic(panic_err)),
206        };
207        match AssertUnwindSafe(future).catch_unwind().await {
208            Ok(res) => match res {
209                Ok(res) => Ok(res.map(Into::into)),
210                Err(err) => Err(err),
211            },
212            Err(panic_err) => Ok(self.panic_handler.response_for_panic(panic_err)),
213        }
214    }
215}
216
217/// Trait for creating responses from panics.
218pub trait ResponseForPanic: Clone {
219    /// Create a response from the panic error.
220    fn response_for_panic(&self, err: Box<dyn Any + Send + 'static>) -> Response<Body>;
221}
222
223impl<F> ResponseForPanic for F
224where
225    F: Fn(Box<dyn Any + Send + 'static>) -> Response + Clone,
226{
227    fn response_for_panic(&self, err: Box<dyn Any + Send + 'static>) -> Response {
228        self(err)
229    }
230}
231
232/// The default `ResponseForPanic` used by `CatchPanic`.
233///
234/// It will log the panic message and return a `500 Internal Server` error response with an empty
235/// body.
236#[derive(Debug, Default, Clone)]
237#[non_exhaustive]
238pub struct DefaultResponseForPanic;
239
240impl ResponseForPanic for DefaultResponseForPanic {
241    fn response_for_panic(&self, err: Box<dyn Any + Send + 'static>) -> Response {
242        if let Some(s) = err.downcast_ref::<String>() {
243            tracing::error!("Service panicked: {}", s);
244        } else if let Some(s) = err.downcast_ref::<&str>() {
245            tracing::error!("Service panicked: {}", s);
246        } else {
247            tracing::error!(
248                "Service panicked but `CatchPanic` was unable to downcast the panic info"
249            );
250        };
251
252        let mut res = Response::new(Body::from("Service panicked"));
253        *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
254
255        #[allow(clippy::declare_interior_mutable_const)]
256        const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8");
257        res.headers_mut()
258            .insert(rama_http_types::header::CONTENT_TYPE, TEXT_PLAIN);
259
260        res
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    #![allow(unreachable_code)]
267
268    use super::*;
269
270    use crate::{Body, Response, body::util::BodyExt};
271    use rama_core::Service;
272    use rama_core::service::service_fn;
273    use std::convert::Infallible;
274
275    #[tokio::test]
276    async fn panic_before_returning_future() {
277        let svc = CatchPanicLayer::new().into_layer(service_fn(|_: Request| {
278            panic!("service panic");
279            async { Ok::<_, Infallible>(Response::new(Body::empty())) }
280        }));
281
282        let req = Request::new(Body::empty());
283
284        let res = svc.serve(req).await.unwrap();
285
286        assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
287        let body = res.into_body().collect().await.unwrap().to_bytes();
288        assert_eq!(&body[..], b"Service panicked");
289    }
290
291    #[tokio::test]
292    async fn panic_in_future() {
293        let svc = CatchPanicLayer::new().into_layer(service_fn(async |_: Request<Body>| {
294            panic!("future panic");
295            Ok::<_, Infallible>(Response::new(Body::empty()))
296        }));
297
298        let req = Request::new(Body::empty());
299
300        let res = svc.serve(req).await.unwrap();
301
302        assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
303        let body = res.into_body().collect().await.unwrap().to_bytes();
304        assert_eq!(&body[..], b"Service panicked");
305    }
306}