Skip to main content

cratestack_axum/headers/
traceparent.rs

1use axum::http::HeaderMap;
2use cratestack_core::CoolError;
3
4/// Extract a W3C `traceparent` header, returning the trace-id portion when
5/// the header is present and well-formed. Returns `Ok(None)` when absent —
6/// callers should mint their own request id in that case so every audit row
7/// carries something. The trace-id is the second hyphen-delimited segment
8/// per [W3C Trace Context]; this implementation does **not** validate the
9/// flags/version segments since banks usually rebuild traceparent at the
10/// edge anyway.
11///
12/// [W3C Trace Context]: https://www.w3.org/TR/trace-context/
13pub fn parse_traceparent(headers: &HeaderMap) -> Result<Option<String>, CoolError> {
14    let Some(value) = headers.get("traceparent") else {
15        return Ok(None);
16    };
17    let raw = value
18        .to_str()
19        .map_err(|_| CoolError::BadRequest("traceparent must be ASCII".to_owned()))?
20        .trim();
21    if raw.is_empty() {
22        return Ok(None);
23    }
24    let parts: Vec<&str> = raw.split('-').collect();
25    if parts.len() != 4 {
26        return Err(CoolError::BadRequest(
27            "traceparent must have 4 hyphen-delimited segments".to_owned(),
28        ));
29    }
30    let trace_id = parts[1];
31    if trace_id.len() != 32 || !trace_id.chars().all(|c| c.is_ascii_hexdigit()) {
32        return Err(CoolError::BadRequest(
33            "traceparent trace-id must be 32 lowercase hex characters".to_owned(),
34        ));
35    }
36    if trace_id == "00000000000000000000000000000000" {
37        return Err(CoolError::BadRequest(
38            "traceparent trace-id must not be all zeros".to_owned(),
39        ));
40    }
41    Ok(Some(trace_id.to_owned()))
42}