Skip to main content

http_extract/
request_id.rs

1//! Request ID extraction with fixed common field precedence.
2//!
3//! `X-Request-Id` is preferred and `Request-Id` is used only as a fallback.
4//! These are configured field names used by this crate, not universal
5//! IETF-standard request ID fields. This module does not generate or validate
6//! identifiers.
7
8use http::{HeaderMap, HeaderName, Request};
9
10use crate::{Error, header::extract_single_header_text};
11
12/// The preferred `X-Request-Id` field name.
13pub const X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
14
15/// The fallback `Request-Id` field name.
16pub const REQUEST_ID: HeaderName = HeaderName::from_static("request-id");
17
18/// Extract a request ID from request fields.
19///
20/// `X-Request-Id` takes precedence over `Request-Id`; the fallback is inspected
21/// only when `X-Request-Id` is absent. If both are absent, this function returns
22/// `None`. The selected value is returned unchanged, so an empty field produces
23/// `Some("")`. A selected field that occurs more than once or is not text
24/// produces an error that does not contain the field value. This function does
25/// not generate, validate, authenticate, or log an identifier.
26pub fn extract_header_request_id(headers: &HeaderMap) -> Result<Option<&str>, Error> {
27    if let Some(value) = extract_single_header_text(headers, &X_REQUEST_ID)? {
28        return Ok(Some(value));
29    }
30    if let Some(value) = extract_single_header_text(headers, &REQUEST_ID)? {
31        return Ok(Some(value));
32    }
33    Ok(None)
34}
35
36/// Extract a request ID from a complete request.
37///
38/// This reads `request.headers()` and delegates to
39/// [`extract_header_request_id`]. It therefore uses the same fixed precedence,
40/// missing and error behavior, preserves empty selected values, and performs no
41/// generation or validation.
42pub fn extract_request_request_id<B>(request: &Request<B>) -> Result<Option<&str>, Error> {
43    extract_header_request_id(request.headers())
44}
45
46#[cfg(test)]
47mod tests {
48    use http::{HeaderMap, HeaderValue, Request};
49
50    use super::*;
51
52    #[test]
53    fn missing_fallback_and_preferred_values_are_distinguished() {
54        let mut headers = HeaderMap::new();
55        assert_eq!(extract_header_request_id(&headers), Ok(None));
56
57        headers.insert("request-id", "fallback".parse().unwrap());
58        assert_eq!(extract_header_request_id(&headers), Ok(Some("fallback")));
59
60        headers.insert("x-request-id", "preferred".parse().unwrap());
61        assert_eq!(extract_header_request_id(&headers), Ok(Some("preferred")));
62    }
63
64    #[test]
65    fn preserves_empty_x_request_id_value_without_falling_back() {
66        let mut headers = HeaderMap::new();
67        headers.insert("request-id", "fallback".parse().unwrap());
68        headers.insert("x-request-id", "".parse().unwrap());
69        assert_eq!(extract_header_request_id(&headers), Ok(Some("")));
70    }
71
72    #[test]
73    fn rejects_duplicate_and_non_text_selected_fields_without_echoing_values() {
74        let mut headers = HeaderMap::new();
75        headers.append("x-request-id", "first-secret".parse().unwrap());
76        headers.append("x-request-id", "second-secret".parse().unwrap());
77        let error = extract_header_request_id(&headers).unwrap_err();
78        assert!(matches!(error, Error::DuplicateHeader { .. }));
79        assert!(!error.to_string().contains("secret"));
80
81        headers.clear();
82        headers.insert("x-request-id", HeaderValue::from_bytes(&[0xff]).unwrap());
83        assert!(matches!(
84            extract_header_request_id(&headers),
85            Err(Error::InvalidHeader { .. })
86        ));
87    }
88
89    #[test]
90    fn request_entry_point_delegates_to_headers() {
91        let request = Request::builder()
92            .header("request-id", "fallback")
93            .body(())
94            .unwrap();
95        assert_eq!(extract_request_request_id(&request), Ok(Some("fallback")));
96    }
97}