Skip to main content

soma_http_server/middleware/
request_id.rs

1//! Request-ID middleware.
2//!
3//! Assigns a UUID to `x-request-id` on the way in (unless the caller already
4//! supplied one) and copies it back onto the response on the way out.
5//! Compose both layers with `tower::ServiceBuilder`: `set` outermost (so
6//! it runs first on the way in) and `propagate` innermost (so it runs last
7//! on the way out, after the response has been fully built, and so
8//! survives handler and error-mapping layers placed between them):
9//!
10//! ```
11//! use tower::ServiceBuilder;
12//! use soma_http_server::middleware::request_id;
13//!
14//! let _middleware = ServiceBuilder::new()
15//!     .layer(request_id::set_request_id_layer())
16//!     .layer(request_id::propagate_request_id_layer());
17//! ```
18//!
19//! Note: [`crate::middleware::tracing::trace_layer`] uses `tower_http`'s
20//! *default* HTTP span, which does **not** automatically include the
21//! request ID. A product wanting the ID inside its trace spans needs a
22//! custom `tower_http::trace::TraceLayer::new_for_http().make_span_with(..)`
23//! that reads the `x-request-id` header itself — layering `trace_layer()`
24//! between `set`/`propagate` alone does not achieve that.
25
26use axum::http::HeaderName;
27pub use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
28
29/// Default request-ID header name shared across Soma HTTP surfaces.
30pub const REQUEST_ID_HEADER: &str = "x-request-id";
31
32fn header_name() -> HeaderName {
33    HeaderName::from_static(REQUEST_ID_HEADER)
34}
35
36/// Layer that assigns a random UUID request ID when the incoming request
37/// does not already carry one.
38pub fn set_request_id_layer() -> SetRequestIdLayer<MakeRequestUuid> {
39    SetRequestIdLayer::new(header_name(), MakeRequestUuid)
40}
41
42/// Layer that copies the request-scoped ID from the request onto the
43/// response headers.
44pub fn propagate_request_id_layer() -> PropagateRequestIdLayer {
45    PropagateRequestIdLayer::new(header_name())
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use axum::{body::Body, http::Request, routing::get, Router};
52    use tower::{ServiceBuilder, ServiceExt};
53
54    // `set_request_id_layer` must be outermost (added first) so it assigns
55    // the ID before anything downstream runs; `propagate_request_id_layer`
56    // must be innermost (added last, closest to the app) so it captures
57    // that ID off the request and copies it onto the response.
58
59    #[tokio::test]
60    async fn assigns_and_propagates_a_request_id_when_absent() {
61        let app = Router::new().route("/", get(|| async { "ok" })).layer(
62            ServiceBuilder::new()
63                .layer(set_request_id_layer())
64                .layer(propagate_request_id_layer()),
65        );
66
67        let request = Request::builder().uri("/").body(Body::empty()).unwrap();
68        let response = app.oneshot(request).await.unwrap();
69        let id = response
70            .headers()
71            .get(REQUEST_ID_HEADER)
72            .expect("response should carry a request id");
73        assert!(!id.is_empty());
74    }
75
76    #[tokio::test]
77    async fn preserves_an_incoming_request_id() {
78        let app = Router::new().route("/", get(|| async { "ok" })).layer(
79            ServiceBuilder::new()
80                .layer(set_request_id_layer())
81                .layer(propagate_request_id_layer()),
82        );
83
84        let request = Request::builder()
85            .uri("/")
86            .header(REQUEST_ID_HEADER, "caller-supplied-id")
87            .body(Body::empty())
88            .unwrap();
89        let response = app.oneshot(request).await.unwrap();
90        assert_eq!(
91            response.headers().get(REQUEST_ID_HEADER).unwrap(),
92            "caller-supplied-id"
93        );
94    }
95}