1use crate::middleware::{named, MwEntry, Next};
4use crate::request::Request;
5use tracing::Instrument;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct RequestId(pub String);
10
11impl std::fmt::Display for RequestId {
12 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 self.0.fmt(f)
14 }
15}
16
17impl AsRef<str> for RequestId {
18 fn as_ref(&self) -> &str {
19 &self.0
20 }
21}
22
23pub fn request_id() -> MwEntry {
25 named("request-id", |mut req: Request, next: Next| async move {
26 ensure_request_id(&mut req);
27 let id = req
28 .get::<RequestId>()
29 .map(|r| r.0.clone())
30 .unwrap_or_default();
31 let method = req.method.as_str().to_string();
32 let path = req.path.clone();
33 let span = tracing::info_span!(
34 "http.server",
35 request_id = %id,
36 method = %method,
37 path = %path,
38 otel.kind = "server",
39 );
40 async move {
41 let mut res = next(req).await;
42 if !id.is_empty() {
43 res = res.header("x-request-id", &id);
44 }
45 res
46 }
47 .instrument(span)
48 .await
49 })
50}
51
52pub fn ensure_request_id(req: &mut Request) {
54 if req.get::<RequestId>().is_some() {
55 return;
56 }
57 let id = req
58 .header("x-request-id")
59 .filter(|s| !s.is_empty())
60 .map(str::to_owned)
61 .unwrap_or_else(generate_request_id);
62 req.set(RequestId(id));
63}
64
65fn generate_request_id() -> String {
66 use std::sync::atomic::{AtomicU64, Ordering};
67 static COUNTER: AtomicU64 = AtomicU64::new(1);
68 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
69 let mut bytes = [0u8; 8];
70 let _ = getrandom::getrandom(&mut bytes);
71 let entropy = u64::from_le_bytes(bytes);
72 format!("req-{entropy:016x}-{n:x}")
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::middleware::build_chain;
79 use crate::response::Response;
80 use http::Method;
81 use std::sync::Arc;
82
83 #[tokio::test]
84 async fn echoes_and_reuses_inbound() {
85 let leaf: crate::handler::Handler = Arc::new(|req: Request| {
86 Box::pin(async move {
87 let id = req.get::<RequestId>().unwrap().0.clone();
88 Response::text(id)
89 })
90 });
91 let chain = build_chain(&[request_id().mw], leaf);
92 let mut req = Request::new(Method::GET, "/");
93 req.headers
94 .insert("x-request-id", "abc-123".parse().unwrap());
95 let res = chain(req).await;
96 assert_eq!(res.body_bytes(), Some(b"abc-123".as_slice()));
97 assert_eq!(
98 res.headers
99 .get("x-request-id")
100 .and_then(|v| v.to_str().ok()),
101 Some("abc-123")
102 );
103 }
104
105 #[tokio::test]
106 async fn generates_when_missing() {
107 let leaf: crate::handler::Handler = Arc::new(|req: Request| {
108 Box::pin(async move {
109 let id = req.get::<RequestId>().unwrap().0.clone();
110 Response::text(id)
111 })
112 });
113 let chain = build_chain(&[request_id().mw.clone()], leaf);
114 let res = chain(Request::new(Method::GET, "/")).await;
115 let body = String::from_utf8(res.body_bytes().unwrap().to_vec()).unwrap();
116 assert!(body.starts_with("req-"), "{body}");
117 assert_eq!(
118 res.headers
119 .get("x-request-id")
120 .and_then(|v| v.to_str().ok()),
121 Some(body.as_str())
122 );
123 }
124}