Skip to main content

http_handle/
async_runtime.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2023 - 2026 HTTP Handle
3
4//! Async runtime helpers for panic-safe blocking execution.
5
6use crate::error::ServerError;
7
8/// Runs a blocking function on Tokio's blocking pool and maps panics/joins to `TaskFailed`.
9#[cfg(feature = "async")]
10#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
11///
12/// # Examples
13///
14/// ```rust,no_run
15/// use http_handle::async_runtime::run_blocking;
16/// use http_handle::ServerError;
17/// # #[tokio::main(flavor = "current_thread")]
18/// # async fn main() -> Result<(), ServerError> {
19/// let value = run_blocking(|| Ok::<_, ServerError>(42)).await?;
20/// assert_eq!(value, 42);
21/// # Ok(())
22/// # }
23/// ```
24///
25/// # Errors
26///
27/// Returns the operation error or `TaskFailed` when the blocking task panics or join fails.
28///
29/// # Panics
30///
31/// This function does not panic.
32pub async fn run_blocking<F, T>(operation: F) -> Result<T, ServerError>
33where
34    F: FnOnce() -> Result<T, ServerError> + Send + 'static,
35    T: Send + 'static,
36{
37    match tokio::task::spawn_blocking(operation).await {
38        Ok(result) => result,
39        Err(err) => Err(ServerError::TaskFailed(format!(
40            "blocking task failed: {err}"
41        ))),
42    }
43}
44
45/// Non-async fallback for builds without async feature.
46#[cfg(not(feature = "async"))]
47///
48/// # Examples
49///
50/// ```rust
51/// use http_handle::async_runtime::run_blocking;
52/// use http_handle::ServerError;
53/// let value = run_blocking(|| Ok::<_, ServerError>(7)).expect("ok");
54/// assert_eq!(value, 7);
55/// ```
56///
57/// # Errors
58///
59/// Returns the operation error.
60///
61/// # Panics
62///
63/// This function does not panic.
64pub fn run_blocking<F, T>(operation: F) -> Result<T, ServerError>
65where
66    F: FnOnce() -> Result<T, ServerError>,
67{
68    operation()
69}
70
71#[cfg(all(test, feature = "async"))]
72mod tests {
73    use super::*;
74
75    #[tokio::test]
76    async fn run_blocking_maps_panic_to_task_failed() {
77        let result = run_blocking(|| -> Result<(), ServerError> {
78            panic!("boom")
79        })
80        .await;
81        let err =
82            result.expect_err("blocking panic must surface as Err");
83        // Direct discriminant compare avoids the `assert!(matches!(...))`
84        // macro expansion which leaves an uncovered sub-region for the
85        // implicit "did not match" arm.
86        let is_task_failed = matches!(err, ServerError::TaskFailed(_));
87        assert!(is_task_failed, "unexpected variant: {err:?}");
88    }
89
90    #[tokio::test]
91    async fn run_blocking_returns_inner_error() {
92        let result = run_blocking(|| -> Result<(), ServerError> {
93            Err(ServerError::Custom("inner".to_string()))
94        })
95        .await;
96        let err = result.expect_err("inner Err must propagate");
97        let is_custom = matches!(err, ServerError::Custom(_));
98        assert!(is_custom, "unexpected variant: {err:?}");
99    }
100
101    #[tokio::test]
102    async fn run_blocking_returns_success_value() {
103        let result =
104            run_blocking(|| -> Result<usize, ServerError> { Ok(7) })
105                .await
106                .expect("ok");
107        assert_eq!(result, 7);
108    }
109}