Skip to main content

cratestack_axum/idempotency/
headers.rs

1//! Response-header blob: encode/decode the persisted header set so a
2//! replay reproduces the original handler's `Location`, `ETag`, cache
3//! directives, `Content-Type`, etc.
4
5/// Headers excluded from the replay blob. `Date` should always reflect
6/// when the response is actually emitted; `Content-Length` is recomputed
7/// by the framework from the buffered body so capturing it would risk
8/// mismatch with `Vec<u8>::len()` on the path back out. `Connection` and
9/// `Transfer-Encoding` are hop-by-hop and meaningless to replay.
10const HEADERS_NEVER_REPLAYED: &[&str] =
11    &["content-length", "connection", "transfer-encoding", "date"];
12
13fn is_replayable_header(name: &http::HeaderName) -> bool {
14    !HEADERS_NEVER_REPLAYED.contains(&name.as_str())
15}
16
17/// Encode a response's headers into the opaque blob that the store
18/// persists. Format: little-endian length-prefixed `(name, value)` pairs.
19/// Header values can carry arbitrary bytes (per RFC 9110 they may include
20/// any opaque-data octet, with the exception of CR/LF), so a binary blob
21/// is the only correct representation — JSON would force lossy UTF-8
22/// coercion on values like opaque `ETag` tokens that may already be
23/// quoted-string blobs.
24pub fn encode_headers(headers: &http::HeaderMap) -> Vec<u8> {
25    let mut iter = headers
26        .iter()
27        .filter(|(name, _)| is_replayable_header(name));
28    // Two passes so we can write the count up front; HeaderMap iter
29    // doesn't expose a stable count that excludes filtered entries.
30    let pairs: Vec<_> = iter.by_ref().collect();
31    let mut blob = Vec::with_capacity(4 + pairs.len() * 16);
32    let count = pairs.len() as u32;
33    blob.extend_from_slice(&count.to_le_bytes());
34    for (name, value) in pairs {
35        let name_bytes = name.as_str().as_bytes();
36        let value_bytes = value.as_bytes();
37        blob.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
38        blob.extend_from_slice(name_bytes);
39        blob.extend_from_slice(&(value_bytes.len() as u32).to_le_bytes());
40        blob.extend_from_slice(value_bytes);
41    }
42    blob
43}
44
45/// Decode a blob produced by [`encode_headers`] back into a `HeaderMap`.
46/// Returns an empty map on malformed input rather than failing the
47/// replay — a corrupt headers blob is a recoverable curiosity, not a
48/// reason to drop the response status and body the caller is waiting
49/// for.
50pub fn decode_headers(blob: &[u8]) -> http::HeaderMap {
51    let mut headers = http::HeaderMap::new();
52    if blob.is_empty() {
53        return headers;
54    }
55    let mut cursor = 0;
56    let read_u32 = |bytes: &[u8], offset: usize| -> Option<usize> {
57        bytes
58            .get(offset..offset + 4)
59            .map(|b| u32::from_le_bytes(b.try_into().expect("4-byte slice")) as usize)
60    };
61    let Some(count) = read_u32(blob, cursor) else {
62        return headers;
63    };
64    cursor += 4;
65    for _ in 0..count {
66        let Some(name_len) = read_u32(blob, cursor) else {
67            return headers;
68        };
69        cursor += 4;
70        let Some(name_bytes) = blob.get(cursor..cursor + name_len) else {
71            return headers;
72        };
73        cursor += name_len;
74        let Some(value_len) = read_u32(blob, cursor) else {
75            return headers;
76        };
77        cursor += 4;
78        let Some(value_bytes) = blob.get(cursor..cursor + value_len) else {
79            return headers;
80        };
81        cursor += value_len;
82        let Ok(name) = http::HeaderName::from_bytes(name_bytes) else {
83            continue;
84        };
85        let Ok(value) = http::HeaderValue::from_bytes(value_bytes) else {
86            continue;
87        };
88        // `append`, not `insert`: preserves multi-valued headers like
89        // `Set-Cookie` exactly as the handler emitted them.
90        headers.append(name, value);
91    }
92    headers
93}