Skip to main content

cratefield_core/
scope.rs

1//! Per-request scope (ADR 0007): travels in axum request extensions, never
2//! in shared mutable state. The discarded TypeScript v1 kept "the current
3//! request" in a closure variable and two concurrent requests swapped ids;
4//! Rust makes the same mistake possible with `thread_local!` or a `static`
5//! `RefCell` — this module is the cure, and the concurrency test in
6//! `tests/router.rs` is the regression test.
7
8use axum::extract::FromRequestParts;
9use axum::http::request::Parts;
10use std::future::Future;
11use std::sync::Arc;
12use tracing::Span;
13
14use crate::ports::Defer;
15use crate::problem::Problem;
16
17/// Per-request scope, inserted into extensions by the request-id layer
18/// (issue #2). Handlers receive it through the `Scope` extractor; there is
19/// no ambient "current request".
20#[derive(Clone)]
21pub struct Scope {
22    pub request_id: String,
23    pub defer: Arc<dyn Defer>,
24    pub span: Span,
25}
26
27impl std::fmt::Debug for Scope {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_struct("Scope")
30            .field("request_id", &self.request_id)
31            .field("span", &self.span.metadata().map(tracing::Metadata::name))
32            .finish_non_exhaustive()
33    }
34}
35
36impl<S> FromRequestParts<S> for Scope
37where
38    S: Send + Sync,
39{
40    type Rejection = Problem;
41
42    fn from_request_parts(
43        parts: &mut Parts,
44        _state: &S,
45    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
46        std::future::ready(
47            parts
48                .extensions
49                .get::<Scope>()
50                .cloned()
51                .ok_or_else(Problem::internal),
52        )
53    }
54}