Skip to main content

tower_http_cache/
request_id.rs

1//! Request ID infrastructure for request correlation and tracing.
2//!
3//! This module provides a `RequestId` type for tracking requests across
4//! the caching layer and downstream services. Request IDs can be extracted
5//! from headers (e.g., X-Request-ID) or generated automatically.
6
7use http::{HeaderValue, Request};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::str::FromStr;
12use uuid::Uuid;
13
14/// Unique identifier for tracking a request through the system.
15///
16/// Request IDs enable correlation of logs, metrics, and traces across
17/// different components and services. They can be extracted from incoming
18/// headers or generated automatically.
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub struct RequestId(String);
22
23impl RequestId {
24    /// Generates a new random request ID using UUID v4.
25    pub fn new() -> Self {
26        Self(Uuid::new_v4().to_string())
27    }
28
29    /// Creates a request ID from a string value.
30    ///
31    /// This is useful when extracting request IDs from headers or
32    /// other sources. For a fallible variant, use `RequestId::from_str()` from the `FromStr` trait.
33    pub fn from_string(s: impl Into<String>) -> Self {
34        Self(s.into())
35    }
36
37    /// Attempts to extract a request ID from an HTTP header.
38    ///
39    /// Returns `None` if the header value is not valid UTF-8.
40    pub fn from_header(header: &HeaderValue) -> Option<Self> {
41        header.to_str().ok().map(|s| Self(s.to_owned()))
42    }
43
44    /// Extracts a request ID from the request headers, or generates a new one.
45    ///
46    /// Looks for the `X-Request-ID` header first. If not present or invalid,
47    /// generates a new random ID.
48    pub fn from_request<B>(req: &Request<B>) -> Self {
49        req.headers()
50            .get("x-request-id")
51            .and_then(Self::from_header)
52            .unwrap_or_default()
53    }
54
55    /// Returns the request ID as a string slice.
56    pub fn as_str(&self) -> &str {
57        &self.0
58    }
59
60    /// Consumes the request ID and returns the inner string.
61    pub fn into_string(self) -> String {
62        self.0
63    }
64}
65
66impl Default for RequestId {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl fmt::Display for RequestId {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "{}", self.0)
75    }
76}
77
78impl From<String> for RequestId {
79    fn from(s: String) -> Self {
80        Self(s)
81    }
82}
83
84impl From<RequestId> for String {
85    fn from(id: RequestId) -> Self {
86        id.0
87    }
88}
89
90impl FromStr for RequestId {
91    type Err = std::convert::Infallible;
92
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        Ok(Self(s.to_owned()))
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use http::Request;
102
103    #[test]
104    fn new_generates_valid_uuid() {
105        let id1 = RequestId::new();
106        let id2 = RequestId::new();
107        assert_ne!(id1, id2);
108        assert!(Uuid::parse_str(id1.as_str()).is_ok());
109    }
110
111    #[test]
112    fn from_str_creates_request_id() {
113        let id = "custom-id-123".parse::<RequestId>().unwrap();
114        assert_eq!(id.as_str(), "custom-id-123");
115    }
116
117    #[test]
118    fn from_header_extracts_valid_value() {
119        let header = HeaderValue::from_static("test-request-id");
120        let id = RequestId::from_header(&header).unwrap();
121        assert_eq!(id.as_str(), "test-request-id");
122    }
123
124    #[test]
125    fn from_request_extracts_header() {
126        let mut req = Request::builder().body(()).unwrap();
127        req.headers_mut()
128            .insert("x-request-id", HeaderValue::from_static("header-id"));
129
130        let id = RequestId::from_request(&req);
131        assert_eq!(id.as_str(), "header-id");
132    }
133
134    #[test]
135    fn from_request_generates_when_missing() {
136        let req = Request::builder().body(()).unwrap();
137        let id = RequestId::from_request(&req);
138        assert!(Uuid::parse_str(id.as_str()).is_ok());
139    }
140
141    #[test]
142    fn display_implementation() {
143        let id = "test-id".parse::<RequestId>().unwrap();
144        assert_eq!(format!("{}", id), "test-id");
145    }
146
147    #[test]
148    fn string_conversions() {
149        let original = "test-id".to_string();
150        let id = RequestId::from(original.clone());
151        let converted: String = id.into();
152        assert_eq!(original, converted);
153    }
154}