poll-scope exposes an immutable, request-scoped current context that can be
read anywhere in the future handling a request. It is poll-scoped rather than
thread-local, so it remains correct when Tokio moves a request future between
worker threads.
use poll_scope::{current, scope};
#[derive(Clone)]
struct RequestContext { request_id: String, user_id: Option<u64> }
async fn repository() -> String {
current::<RequestContext>().unwrap().request_id.clone()
}
# async fn example() {
let request = RequestContext { request_id: "req-1".into(), user_id: None };
let id = scope(request, repository()).await;
assert_eq!(id, "req-1");
# }Middleware can progressively enrich the context without shared mutable state:
async fn authentication(req: Request, next: Next) -> Response {
let parent = current::<RequestContext>().unwrap();
let child = RequestContext { user_id: Some(authenticate(&req).await?), ..(*parent).clone() };
scope(child, next.run(req)).await
}The tower feature exposes ContextLayer, for contexts that can be built
synchronously from each request:
Router::new()
.route("/", get(handler))
.layer(ContextLayer::new(|request| RequestContext::from_request(request)));tokio::spawn does not inherit a scope. Enable the tokio feature and use
spawn_with_current::<RequestContext>(future) to explicitly carry one current
context type into a background task. Alternatively, pass a value explicitly and
create a new scope within the spawned future.