use http::Version;
use tracing::debug;
fn via_value(version: Version) -> &'static str {
match version {
Version::HTTP_09 => "0.9 praxis",
Version::HTTP_10 => "1.0 praxis",
Version::HTTP_11 => "1.1 praxis",
Version::HTTP_2 => "2 praxis",
Version::HTTP_3 => "3 praxis",
_ => {
tracing::warn!(?version, "unknown HTTP version in Via header, defaulting to 1.1");
"1.1 praxis"
},
}
}
fn via_header_value(entry: &'static str) -> http::HeaderValue {
const V09: http::HeaderValue = http::HeaderValue::from_static("0.9 praxis");
const V10: http::HeaderValue = http::HeaderValue::from_static("1.0 praxis");
const V11: http::HeaderValue = http::HeaderValue::from_static("1.1 praxis");
const V2: http::HeaderValue = http::HeaderValue::from_static("2 praxis");
const V3: http::HeaderValue = http::HeaderValue::from_static("3 praxis");
match entry {
"0.9 praxis" => V09,
"1.0 praxis" => V10,
"2 praxis" => V2,
"3 praxis" => V3,
_ => V11,
}
}
fn combined_via(headers: &http::HeaderMap, entry: &str) -> Option<String> {
let mut existing: Vec<&str> = Vec::new();
for value in headers.get_all("via") {
let text = value.to_str().ok()?;
if !text.is_empty() {
existing.push(text);
}
}
(!existing.is_empty()).then(|| format!("{}, {entry}", existing.join(", ")))
}
pub(crate) fn append_request_via(req: &mut pingora_http::RequestHeader, upstream_version: Version) {
let entry = via_value(upstream_version);
if let Some(combined) = combined_via(&req.headers, entry) {
debug!(via = %combined, "appending to existing request Via");
let _insert = req.insert_header("via", combined);
} else {
debug!(via = %entry, "adding request Via header");
let _insert = req.insert_header("via", via_header_value(entry));
}
}
pub(crate) fn append_response_via(resp: &mut pingora_http::ResponseHeader, upstream_version: Version) {
let entry = via_value(upstream_version);
if let Some(combined) = combined_via(&resp.headers, entry) {
debug!(via = %combined, "appending to existing response Via");
let _insert = resp.insert_header("via", combined);
} else {
debug!(via = %entry, "adding response Via header");
let _insert = resp.insert_header("via", via_header_value(entry));
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::field_reassign_with_default,
reason = "tests"
)]
mod tests {
use http::HeaderValue;
use super::*;
#[test]
fn via_value_http11() {
assert_eq!(
via_value(Version::HTTP_11),
"1.1 praxis",
"Via value for HTTP/1.1 should be '1.1 praxis'"
);
}
#[test]
fn via_value_http2() {
assert_eq!(
via_value(Version::HTTP_2),
"2 praxis",
"Via value for HTTP/2 should be '2 praxis'"
);
}
#[test]
fn append_request_via_new_header() {
let mut req = pingora_http::RequestHeader::build("GET", b"/", None).unwrap();
append_request_via(&mut req, Version::HTTP_11);
assert_eq!(
req.headers.get("via").unwrap(),
"1.1 praxis",
"new Via header should be set on request"
);
}
#[test]
fn append_request_via_existing_header() {
let mut req = pingora_http::RequestHeader::build("GET", b"/", None).unwrap();
let _insert = req.insert_header("via", "1.0 downstream-proxy");
append_request_via(&mut req, Version::HTTP_11);
assert_eq!(
req.headers.get("via").unwrap(),
"1.0 downstream-proxy, 1.1 praxis",
"Via should be appended to existing value"
);
}
#[test]
fn append_response_via_new_header() {
let mut resp = pingora_http::ResponseHeader::build(200, None).unwrap();
append_response_via(&mut resp, Version::HTTP_11);
assert_eq!(
resp.headers.get("via").unwrap(),
"1.1 praxis",
"new Via header should be set on response"
);
}
#[test]
fn append_response_via_existing_header() {
let mut resp = pingora_http::ResponseHeader::build(200, None).unwrap();
let _insert = resp.insert_header("via", "1.1 upstream-proxy");
append_response_via(&mut resp, Version::HTTP_11);
assert_eq!(
resp.headers.get("via").unwrap(),
"1.1 upstream-proxy, 1.1 praxis",
"Via should be appended to existing response value"
);
}
#[test]
fn append_request_via_replaces_non_utf8() {
let mut req = pingora_http::RequestHeader::build("GET", b"/", None).unwrap();
let _insert = req.insert_header("via", HeaderValue::from_bytes(&[0x80, 0xFF]).unwrap());
append_request_via(&mut req, Version::HTTP_11);
assert_eq!(
req.headers.get("via").unwrap(),
"1.1 praxis",
"non-UTF8 Via should be replaced, not appended to"
);
}
#[test]
fn append_response_via_replaces_non_utf8() {
let mut resp = pingora_http::ResponseHeader::build(200, None).unwrap();
let _insert = resp.insert_header("via", HeaderValue::from_bytes(&[0x80, 0xFF]).unwrap());
append_response_via(&mut resp, Version::HTTP_11);
assert_eq!(
resp.headers.get("via").unwrap(),
"1.1 praxis",
"non-UTF8 Via should be replaced, not appended to"
);
}
#[test]
fn append_request_via_h2() {
let mut req = pingora_http::RequestHeader::build("GET", b"/", None).unwrap();
append_request_via(&mut req, Version::HTTP_2);
assert_eq!(
req.headers.get("via").unwrap(),
"2 praxis",
"HTTP/2 request Via should use '2' token"
);
}
#[test]
fn append_response_via_h2() {
let mut resp = pingora_http::ResponseHeader::build(200, None).unwrap();
append_response_via(&mut resp, Version::HTTP_2);
assert_eq!(
resp.headers.get("via").unwrap(),
"2 praxis",
"HTTP/2 response Via should use '2' token"
);
}
#[test]
fn append_request_via_combines_multiple_field_lines() {
let mut req = pingora_http::RequestHeader::build("GET", b"/", None).unwrap();
req.append_header("via", "1.0 a").unwrap();
req.append_header("via", "1.1 b").unwrap();
append_request_via(&mut req, Version::HTTP_11);
assert_eq!(
req.headers.get("via").unwrap(),
"1.0 a, 1.1 b, 1.1 praxis",
"all existing request Via field-lines must be preserved"
);
}
#[test]
fn append_response_via_combines_multiple_field_lines() {
let mut resp = pingora_http::ResponseHeader::build(200, None).unwrap();
resp.append_header("via", "1.1 a").unwrap();
resp.append_header("via", "2 b").unwrap();
append_response_via(&mut resp, Version::HTTP_11);
assert_eq!(
resp.headers.get("via").unwrap(),
"1.1 a, 2 b, 1.1 praxis",
"all existing response Via field-lines must be preserved"
);
}
#[test]
fn append_request_via_replaces_when_any_line_non_utf8() {
let mut req = pingora_http::RequestHeader::build("GET", b"/", None).unwrap();
req.append_header("via", "1.0 a").unwrap();
req.append_header("via", HeaderValue::from_bytes(&[0x80, 0xFF]).unwrap())
.unwrap();
append_request_via(&mut req, Version::HTTP_11);
assert_eq!(
req.headers.get("via").unwrap(),
"1.1 praxis",
"a non-UTF-8 field-line forces outright replacement"
);
}
#[test]
fn append_response_via_replaces_when_any_line_non_utf8() {
let mut resp = pingora_http::ResponseHeader::build(200, None).unwrap();
resp.append_header("via", "1.0 a").unwrap();
resp.append_header("via", HeaderValue::from_bytes(&[0x80, 0xFF]).unwrap())
.unwrap();
append_response_via(&mut resp, Version::HTTP_11);
assert_eq!(
resp.headers.get("via").unwrap(),
"1.1 praxis",
"a non-UTF-8 response field-line forces outright replacement"
);
}
}