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}