#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlsError {
NotSet,
}
impl std::fmt::Display for AlsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AlsError::NotSet => f.write_str(
"async-local-storage value is not installed on this task \
— middleware should set it via `with_<name>(value, future).await`",
),
}
}
}
impl std::error::Error for AlsError {}
impl axum::response::IntoResponse for AlsError {
fn into_response(self) -> axum::response::Response {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
self.to_string(),
)
.into_response()
}
}
pub use tokio::task_local;
pub use async_trait::async_trait;
pub type AlsCell<T> = tokio::task::LocalKey<std::cell::RefCell<Option<T>>>;
pub struct AlsContext<T>
where
T: Clone + Send + Sync + 'static,
{
cell: &'static AlsCell<T>,
}
impl<T> AlsContext<T>
where
T: Clone + Send + Sync + 'static,
{
pub fn new(cell: &'static AlsCell<T>) -> Self {
Self { cell }
}
pub async fn with<F, R>(&self, value: T, future: F) -> R
where
F: std::future::Future<Output = R>,
{
self.cell
.scope(std::cell::RefCell::new(Some(value)), future)
.await
}
pub fn current(&self) -> Option<T> {
self.cell
.try_with(|cell| cell.borrow().clone())
.ok()
.flatten()
}
}
#[cfg(test)]
mod tests {
use super::*;
tokio::task_local! {
static CTX: std::cell::RefCell<Option<String>>;
}
#[tokio::test]
async fn als_context_with_installs_value_for_future_duration() {
let als = AlsContext::new(&CTX);
assert!(als.current().is_none(), "empty before scope");
let inside = als
.with(String::from("hello"), async { als.current() })
.await;
assert_eq!(inside.as_deref(), Some("hello"));
assert!(als.current().is_none(), "empty after scope ends");
}
#[tokio::test]
async fn als_context_with_restores_after_panic() {
let als = AlsContext::new(&CTX);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
rt.block_on(async {
let _ = als
.with(String::from("outer"), async {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
rt.block_on(async {
let _ = als
.with(String::from("inner"), async {
panic!("simulated panic");
})
.await;
});
}));
assert!(result.is_err(), "inner panic propagated");
als.current()
})
.await;
});
}));
assert!(als.current().is_none(), "outer scope restored after panic");
}
#[test]
fn als_error_display_is_actionable() {
let err = AlsError::NotSet;
let msg = format!("{err}");
assert!(
msg.contains("not installed"),
"the error message should hint at the fix: {msg}"
);
}
}