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