Skip to main content

openkind_api/middleware/
request_id.rs

1//! Request ID header stamping and sanitization middleware.
2
3use axum::{
4    body::Body,
5    http::{HeaderName, HeaderValue, Request},
6    middleware::Next,
7    response::Response,
8};
9
10/// Name of the request-id header. Mirrors the SDK's `request_id` property.
11pub const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-typesafe-request-id");
12
13/// Maximum allowed length for an inbound client request ID.
14pub const MAX_REQUEST_ID_LEN: usize = 128;
15
16/// Validate whether a request ID string contains only safe identifier characters.
17pub fn is_safe_request_id(s: &str) -> bool {
18    !s.is_empty()
19        && s.len() <= MAX_REQUEST_ID_LEN
20        && s.chars()
21            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
22}
23
24/// Stored in request extensions by `request_id_layer`.
25#[derive(Debug, Clone)]
26pub struct RequestId(pub String);
27
28/// Stack-format a UUIDv4 without the `Display` machinery, which dominates
29/// this layer's per-request cost.
30fn new_request_id() -> String {
31    let uuid = uuid::Uuid::new_v4();
32    let mut buffer = [0u8; uuid::fmt::Hyphenated::LENGTH];
33    uuid.hyphenated().encode_lower(&mut buffer);
34    // UUIDs are ASCII by construction; the check is free compared to fmt.
35    std::str::from_utf8(&buffer)
36        .expect("hyphenated UUID is ASCII")
37        .to_owned()
38}
39
40/// Stackable middleware function: stamp every response with a request id.
41pub async fn request_id_layer(mut req: Request<Body>, next: Next) -> Response {
42    // Honor an inbound id if the client supplied a valid and safe one
43    // (lets a proxy thread the id through); otherwise mint a fresh UUIDv4.
44    let id = req
45        .headers()
46        .get(&REQUEST_ID_HEADER)
47        .and_then(|v| v.to_str().ok())
48        .filter(|s| is_safe_request_id(s))
49        .map(|s| s.to_string())
50        .unwrap_or_else(new_request_id);
51
52    req.extensions_mut().insert(RequestId(id.clone()));
53
54    let mut resp = next.run(req).await;
55    if let Ok(v) = HeaderValue::from_str(&id) {
56        resp.headers_mut().insert(REQUEST_ID_HEADER.clone(), v);
57    }
58    resp
59}