Skip to main content

php_lsp/
panic_guard.rs

1//! Wrap async LSP handler bodies so a panic in one request doesn't terminate
2//! the server connection. tower-lsp 0.20 does not catch handler panics by
3//! default; a single bad request would otherwise kill the editor's LSP
4//! session and lose unsaved client-side state.
5//!
6//! Usage:
7//! ```ignore
8//! async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
9//!     guard_async("hover", async move {
10//!         // existing handler body
11//!         Ok(...)
12//!     })
13//!     .await
14//! }
15//! ```
16//!
17//! On panic the closure returns `R::default()`, which for the LSP handler
18//! types we care about is `Ok(None)` / `Ok(Vec::new())` / `()` — the
19//! editor sees an empty response, not a closed connection.
20
21use std::future::Future;
22use std::panic::AssertUnwindSafe;
23
24use futures::FutureExt;
25
26fn log_panic(handler_name: &'static str, panic: Box<dyn std::any::Any + Send>) {
27    let msg = if let Some(s) = panic.downcast_ref::<&'static str>() {
28        (*s).to_string()
29    } else if let Some(s) = panic.downcast_ref::<String>() {
30        s.clone()
31    } else {
32        "non-string panic payload".to_string()
33    };
34    tracing::error!(handler = handler_name, panic = msg, "handler panicked");
35}
36
37/// Run `fut` with panic isolation. On panic, log and return the default
38/// value of `R`. Use for notification handlers (return `()`) and other
39/// places where `R: Default` directly applies.
40pub async fn guard_async<F, R>(handler_name: &'static str, fut: F) -> R
41where
42    F: Future<Output = R>,
43    R: Default,
44{
45    match AssertUnwindSafe(fut).catch_unwind().await {
46        Ok(r) => r,
47        Err(panic) => {
48            log_panic(handler_name, panic);
49            R::default()
50        }
51    }
52}
53
54/// Run `fut` that returns `Result<R, E>` with panic isolation. On panic,
55/// log and return `Ok(R::default())` so the LSP client gets a graceful
56/// empty response (typically `null` / empty list) instead of seeing the
57/// connection closed.
58pub async fn guard_async_result<F, R, E>(handler_name: &'static str, fut: F) -> Result<R, E>
59where
60    F: Future<Output = Result<R, E>>,
61    R: Default,
62{
63    match AssertUnwindSafe(fut).catch_unwind().await {
64        Ok(r) => r,
65        Err(panic) => {
66            log_panic(handler_name, panic);
67            Ok(R::default())
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[tokio::test]
77    async fn returns_value_when_no_panic() {
78        let r: i32 = guard_async("test", async { 42 }).await;
79        assert_eq!(r, 42);
80    }
81
82    #[tokio::test]
83    async fn returns_default_on_panic() {
84        let r: i32 = guard_async("test", async { panic!("boom") }).await;
85        assert_eq!(r, 0);
86    }
87
88    #[tokio::test]
89    async fn returns_default_on_panic_for_option() {
90        let r: Option<i32> = guard_async("test", async { panic!("boom") }).await;
91        assert_eq!(r, None);
92    }
93
94    #[tokio::test]
95    async fn result_returns_ok_default_on_panic() {
96        let r: Result<Option<i32>, &str> =
97            guard_async_result("test", async { panic!("boom") }).await;
98        assert_eq!(r, Ok(None));
99    }
100
101    #[tokio::test]
102    async fn result_propagates_err_when_no_panic() {
103        let r: Result<Option<i32>, &str> = guard_async_result("test", async { Err("nope") }).await;
104        assert_eq!(r, Err("nope"));
105    }
106}