rama_http/layer/
catch_panic.rs1#![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#[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 #[must_use]
119 pub const fn new() -> Self {
120 Self {
121 panic_handler: DefaultResponseForPanic,
122 }
123 }
124}
125
126impl<T> CatchPanicLayer<T> {
127 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#[derive(Debug, Clone)]
161pub struct CatchPanic<S, T> {
162 inner: S,
163 panic_handler: T,
164}
165
166impl<S> CatchPanic<S, DefaultResponseForPanic> {
167 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 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
217pub trait ResponseForPanic: Clone {
219 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#[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}