use std::future::Future;
use std::panic::AssertUnwindSafe;
use futures::FutureExt;
fn log_panic(handler_name: &'static str, panic: Box<dyn std::any::Any + Send>) {
let msg = if let Some(s) = panic.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else {
"non-string panic payload".to_string()
};
tracing::error!(handler = handler_name, panic = msg, "handler panicked");
}
pub async fn guard_async<F, R>(handler_name: &'static str, fut: F) -> R
where
F: Future<Output = R>,
R: Default,
{
match AssertUnwindSafe(fut).catch_unwind().await {
Ok(r) => r,
Err(panic) => {
log_panic(handler_name, panic);
R::default()
}
}
}
pub async fn guard_async_result<F, R, E>(handler_name: &'static str, fut: F) -> Result<R, E>
where
F: Future<Output = Result<R, E>>,
R: Default,
{
match AssertUnwindSafe(fut).catch_unwind().await {
Ok(r) => r,
Err(panic) => {
log_panic(handler_name, panic);
Ok(R::default())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn returns_value_when_no_panic() {
let r: i32 = guard_async("test", async { 42 }).await;
assert_eq!(r, 42);
}
#[tokio::test]
async fn returns_default_on_panic() {
let r: i32 = guard_async("test", async { panic!("boom") }).await;
assert_eq!(r, 0);
}
#[tokio::test]
async fn returns_default_on_panic_for_option() {
let r: Option<i32> = guard_async("test", async { panic!("boom") }).await;
assert_eq!(r, None);
}
#[tokio::test]
async fn result_returns_ok_default_on_panic() {
let r: Result<Option<i32>, &str> =
guard_async_result("test", async { panic!("boom") }).await;
assert_eq!(r, Ok(None));
}
#[tokio::test]
async fn result_propagates_err_when_no_panic() {
let r: Result<Option<i32>, &str> = guard_async_result("test", async { Err("nope") }).await;
assert_eq!(r, Err("nope"));
}
}