actix_web_lab/
panic_reporter.rs1use 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#[derive(Clone)]
39pub struct PanicReporter {
40 cb: PanicCallback,
41}
42
43impl PanicReporter {
44 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#[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 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 (cb)(&panic_err);
109
110 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}