Skip to main content

actix_web_lab/
panic_reporter.rs

1//! Panic reporter middleware.
2//!
3//! See [`PanicReporter`] for docs.
4
5use std::{
6    any::Any,
7    future::{Ready, ready},
8    panic::{self, AssertUnwindSafe},
9    rc::Rc,
10};
11
12use actix_web::dev::{Service, Transform, forward_ready};
13use futures_core::future::LocalBoxFuture;
14use futures_util::FutureExt as _;
15
16type PanicCallback = Rc<dyn Fn(&(dyn Any + Send))>;
17
18/// A middleware that triggers a callback when the worker is panicking.
19///
20/// Mostly useful for logging or metrics publishing. The callback received the object with which
21/// panic was originally invoked to allow down-casting.
22///
23/// # Examples
24///
25/// ```no_run
26/// # use actix_web::App;
27/// use actix_web_lab::middleware::PanicReporter;
28/// # mod metrics {
29/// #   macro_rules! increment_counter {
30/// #       ($tt:tt) => {{}};
31/// #   }
32/// #   pub(crate) use increment_counter;
33/// # }
34///
35/// App::new().wrap(PanicReporter::new(|_| metrics::increment_counter!("panic")))
36///     # ;
37/// ```
38#[derive(Clone)]
39pub struct PanicReporter {
40    cb: PanicCallback,
41}
42
43impl PanicReporter {
44    /// Constructs new panic reporter middleware with `callback`.
45    pub fn new(callback: impl Fn(&(dyn Any + Send)) + 'static) -> Self {
46        Self {
47            cb: Rc::new(callback),
48        }
49    }
50}
51
52impl std::fmt::Debug for PanicReporter {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("PanicReporter")
55            .field("cb", &"<callback>")
56            .finish()
57    }
58}
59
60impl<S, Req> Transform<S, Req> for PanicReporter
61where
62    S: Service<Req>,
63    S::Future: 'static,
64{
65    type Response = S::Response;
66    type Error = S::Error;
67    type Transform = PanicReporterMiddleware<S>;
68    type InitError = ();
69    type Future = Ready<Result<Self::Transform, Self::InitError>>;
70
71    fn new_transform(&self, service: S) -> Self::Future {
72        ready(Ok(PanicReporterMiddleware {
73            service: Rc::new(service),
74            cb: Rc::clone(&self.cb),
75        }))
76    }
77}
78
79/// Middleware service implementation for [`PanicReporter`].
80#[doc(hidden)]
81#[allow(missing_debug_implementations)]
82pub struct PanicReporterMiddleware<S> {
83    service: Rc<S>,
84    cb: PanicCallback,
85}
86
87impl<S, Req> Service<Req> for PanicReporterMiddleware<S>
88where
89    S: Service<Req>,
90    S::Future: 'static,
91{
92    type Response = S::Response;
93    type Error = S::Error;
94    type Future = LocalBoxFuture<'static, Result<S::Response, S::Error>>;
95
96    forward_ready!(service);
97
98    fn call(&self, req: Req) -> Self::Future {
99        let cb = Rc::clone(&self.cb);
100
101        // catch panics in service call
102        AssertUnwindSafe(self.service.call(req))
103            .catch_unwind()
104            .map(move |maybe_res| match maybe_res {
105                Ok(res) => res,
106                Err(panic_err) => {
107                    // invoke callback with panic arg
108                    (cb)(&panic_err);
109
110                    // continue unwinding
111                    panic::resume_unwind(panic_err)
112                }
113            })
114            .boxed_local()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use std::sync::{
121        Arc,
122        atomic::{AtomicBool, Ordering},
123    };
124
125    use actix_web::{
126        App, test,
127        web::{self, ServiceConfig},
128    };
129
130    use super::*;
131
132    fn configure_test_app(cfg: &mut ServiceConfig) {
133        cfg.route("/", web::get().to(|| async { "content" })).route(
134            "/disco",
135            #[allow(unreachable_code)]
136            web::get().to(|| async {
137                panic!("the disco");
138                ""
139            }),
140        );
141    }
142
143    #[actix_web::test]
144    async fn report_when_panics_occur() {
145        let triggered = Arc::new(AtomicBool::new(false));
146
147        let app = App::new()
148            .wrap(PanicReporter::new({
149                let triggered = Arc::clone(&triggered);
150                move |_| {
151                    triggered.store(true, Ordering::SeqCst);
152                }
153            }))
154            .configure(configure_test_app);
155
156        let app = test::init_service(app).await;
157
158        let req = test::TestRequest::with_uri("/").to_request();
159        assert!(app.call(req).await.is_ok());
160        assert!(!triggered.load(Ordering::SeqCst));
161
162        let req = test::TestRequest::with_uri("/disco").to_request();
163        assert!(
164            AssertUnwindSafe(app.call(req))
165                .catch_unwind()
166                .await
167                .is_err()
168        );
169        assert!(triggered.load(Ordering::SeqCst));
170    }
171}