Skip to main content

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