rws_server/request_id/mod.rs
1//! Request ID / correlation ID middleware.
2//!
3//! Distributed tracing ([`crate::otel`]) creates a span per request but
4//! doesn't give handlers a simple, stable identifier to put in their own log
5//! lines — and doesn't propagate one across service boundaries unless the
6//! caller already sends a W3C `traceparent`. [`RequestIdLayer`] fills that
7//! gap: it's a plain string ID, present on both the request (so your handler
8//! can read and log it) and the response (so the caller can log the same
9//! value), that survives even in builds/setups without OpenTelemetry wired up.
10//!
11//! # Example
12//!
13//! ```rust,no_run
14//! # extern crate rws_server as rust_web_server;
15//! use rust_web_server::app::App;
16//! use rust_web_server::core::New;
17//! use rust_web_server::request_id::RequestIdLayer;
18//!
19//! let app = App::new().wrap(RequestIdLayer::new());
20//! ```
21//!
22//! Reading it in a handler — the header is just a normal request header,
23//! visible through any of the usual ways to read one:
24//!
25//! ```rust,no_run
26//! # extern crate rws_server as rust_web_server;
27//! use rust_web_server::request::Request;
28//! use rust_web_server::request_id::DEFAULT_HEADER;
29//!
30//! fn handler(request: &Request) {
31//! let id = request.get_header(DEFAULT_HEADER.to_string()).map(|h| h.value.as_ref()).unwrap_or("");
32//! println!("[{}] handling request", id);
33//! }
34//! ```
35
36#[cfg(test)]
37mod tests;
38
39use std::sync::atomic::{AtomicU64, Ordering};
40use std::time::{SystemTime, UNIX_EPOCH};
41
42use crate::application::Application;
43use crate::header::Header;
44use crate::middleware::Middleware;
45use crate::request::Request;
46use crate::response::Response;
47use crate::server::ConnectionInfo;
48
49/// Default header name used for the request ID, both incoming and outgoing.
50pub const DEFAULT_HEADER: &str = "X-Request-Id";
51
52static ID_COUNTER: AtomicU64 = AtomicU64::new(0);
53
54/// Generates a UUID-v4-*shaped* identifier (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).
55///
56/// Not cryptographically random and not a spec-compliant UUID (version/variant
57/// bits aren't forced) — built from a monotonic counter mixed with the
58/// current time via a splitmix64 finalizer, the same non-crypto technique
59/// already used elsewhere in this crate for unique-but-not-secret IDs (e.g.
60/// session IDs). Good for correlating log lines across services; do not use
61/// this as a security token, session ID, or anywhere uniqueness must be
62/// adversarially guaranteed.
63pub fn generate_request_id() -> String {
64 let nanos = SystemTime::now()
65 .duration_since(UNIX_EPOCH)
66 .map(|d| d.as_nanos() as u64)
67 .unwrap_or(0);
68 let count = ID_COUNTER.fetch_add(1, Ordering::Relaxed);
69
70 let mut x = nanos ^ count.wrapping_mul(0x9e3779b97f4a7c15);
71 x ^= x >> 30;
72 x = x.wrapping_mul(0xbf58476d1ce4e5b9);
73 x ^= x >> 27;
74 x = x.wrapping_mul(0x94d049bb133111eb);
75 x ^= x >> 31;
76
77 let mut y = count ^ nanos.wrapping_mul(0x517cc1b727220a95);
78 y ^= y >> 30;
79 y = y.wrapping_mul(0xbf58476d1ce4e5b9);
80 y ^= y >> 27;
81 y = y.wrapping_mul(0x94d049bb133111eb);
82 y ^= y >> 31;
83
84 format!(
85 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
86 (x >> 32) as u32,
87 ((x >> 16) & 0xffff) as u16,
88 (x & 0xffff) as u16,
89 ((y >> 48) & 0xffff) as u16,
90 y & 0xffff_ffff_ffff,
91 )
92}
93
94/// Middleware that ensures every request/response pair carries a stable
95/// correlation ID.
96///
97/// - If the incoming request already has the header (e.g. set by an
98/// upstream gateway, load balancer, or calling service), that exact value
99/// is kept and echoed back unchanged — this lets one ID follow a request
100/// across multiple services instead of getting a new one at each hop.
101/// - Otherwise, a fresh ID is generated with [`generate_request_id`] and
102/// injected into the request *before* it reaches your handler, so handlers
103/// can read it like any other header.
104/// - The same value is always set on the response, so the caller can log it
105/// too — even if it arrived with no ID and this middleware generated one.
106pub struct RequestIdLayer {
107 header: String,
108}
109
110impl RequestIdLayer {
111 /// Uses the default header name, [`DEFAULT_HEADER`] (`X-Request-Id`).
112 pub fn new() -> Self {
113 RequestIdLayer { header: DEFAULT_HEADER.to_string() }
114 }
115
116 /// Use a different header name, e.g. `"X-Correlation-Id"`.
117 pub fn header(mut self, name: impl Into<String>) -> Self {
118 self.header = name.into();
119 self
120 }
121}
122
123impl Default for RequestIdLayer {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl Middleware for RequestIdLayer {
130 /// Headers only: a streamed body passes through untouched.
131 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
132 true
133 }
134
135
136 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
137 let existing = request.get_header(self.header.clone()).map(|h| h.value.to_string());
138
139 let (id, mut response) = match existing {
140 Some(id) => (id, next.execute(request, connection)?),
141 None => {
142 let id = generate_request_id();
143 let mut req = request.clone();
144 req.headers.push(Header { name: self.header.clone().into(), value: id.clone().into() });
145 (id, next.execute(&req, connection)?)
146 }
147 };
148
149 response.headers.push(Header { name: self.header.clone().into(), value: id.into() });
150 Ok(response)
151 }
152
153 /// In-memory only: safe on the event loop.
154 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
155 true
156 }
157}