1use http::HeaderMap;
10use http::header::{ACCESS_CONTROL_EXPOSE_HEADERS, HeaderValue};
11
12pub const X402_EXPOSED_HEADERS: &str = "Payment-Required, Payment-Response";
14
15pub fn ensure_expose_headers(headers: &mut HeaderMap) {
18 let x402 = HeaderValue::from_static(X402_EXPOSED_HEADERS);
19 match headers.get(ACCESS_CONTROL_EXPOSE_HEADERS) {
20 None => {
21 let _ = headers.insert(ACCESS_CONTROL_EXPOSE_HEADERS, x402);
22 }
23 Some(existing) => {
24 let Ok(existing_str) = existing.to_str() else {
25 let _ = headers.insert(ACCESS_CONTROL_EXPOSE_HEADERS, x402);
26 return;
27 };
28 if existing_str.contains("Payment-Required")
29 && existing_str.contains("Payment-Response")
30 {
31 return;
32 }
33 let merged = format!("{existing_str}, {X402_EXPOSED_HEADERS}");
34 if let Ok(value) = HeaderValue::from_str(&merged) {
35 let _ = headers.insert(ACCESS_CONTROL_EXPOSE_HEADERS, value);
36 }
37 }
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn adds_header_when_absent() {
47 let mut headers = HeaderMap::new();
48 ensure_expose_headers(&mut headers);
49 assert_eq!(
50 headers.get(ACCESS_CONTROL_EXPOSE_HEADERS).unwrap(),
51 X402_EXPOSED_HEADERS,
52 );
53 }
54
55 #[test]
56 fn merges_existing_header() {
57 let mut headers = HeaderMap::new();
58 let _ = headers.insert(
59 ACCESS_CONTROL_EXPOSE_HEADERS,
60 HeaderValue::from_static("X-Foo"),
61 );
62 ensure_expose_headers(&mut headers);
63 let value = headers.get(ACCESS_CONTROL_EXPOSE_HEADERS).unwrap();
64 let value = value.to_str().unwrap();
65 assert!(value.contains("X-Foo"));
66 assert!(value.contains("Payment-Required"));
67 assert!(value.contains("Payment-Response"));
68 }
69
70 #[test]
71 fn idempotent_on_repeated_calls() {
72 let mut headers = HeaderMap::new();
73 ensure_expose_headers(&mut headers);
74 ensure_expose_headers(&mut headers);
75 let value = headers.get(ACCESS_CONTROL_EXPOSE_HEADERS).unwrap();
76 assert_eq!(value, X402_EXPOSED_HEADERS);
77 }
78}