pub mod multi_value_headers {
use serde::Deserialize;
use serde::de::{Deserializer, MapAccess, Visitor};
use serde::ser::{SerializeMap, Serializer};
use std::collections::HashMap;
use std::fmt;
pub fn serialize<S: Serializer>(
headers: &HashMap<String, Vec<String>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(Some(headers.len()))?;
for (key, values) in headers {
match values.as_slice() {
[] => continue, [single] => map.serialize_entry(key, single)?,
many => map.serialize_entry(key, many)?,
}
}
map.end()
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Scalar {
Str(String),
Num(serde_json::Number),
Bool(bool),
}
impl Scalar {
fn into_string(self) -> String {
match self {
Scalar::Str(s) => s,
Scalar::Num(n) => n.to_string(),
Scalar::Bool(b) => b.to_string(),
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(Scalar),
Many(Vec<Scalar>),
}
impl OneOrMany {
fn into_strings(self) -> Vec<String> {
match self {
OneOrMany::One(s) => vec![s.into_string()],
OneOrMany::Many(v) => v.into_iter().map(Scalar::into_string).collect(),
}
}
}
struct FoldingVisitor;
impl<'de> Visitor<'de> for FoldingVisitor {
type Value = HashMap<String, Vec<String>>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a map of header names to a scalar or an array of scalars")
}
fn visit_map<M: MapAccess<'de>>(self, mut access: M) -> Result<Self::Value, M::Error> {
let mut folded: Vec<(String, Vec<String>)> =
Vec::with_capacity(access.size_hint().unwrap_or(0).min(1024));
while let Some((name, value)) = access.next_entry::<String, OneOrMany>()? {
let values = value.into_strings();
match folded
.iter_mut()
.find(|(k, _)| k.eq_ignore_ascii_case(&name))
{
Some((_, existing)) => existing.extend(values),
None => folded.push((name, values)),
}
}
Ok(folded.into_iter().collect())
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<HashMap<String, Vec<String>>, D::Error> {
deserializer.deserialize_map(FoldingVisitor)
}
}
pub mod single_value_headers {
use serde::de::{Deserializer, Error, MapAccess, Visitor};
use std::collections::HashMap;
use std::fmt;
struct OneEachVisitor;
impl<'de> Visitor<'de> for OneEachVisitor {
type Value = HashMap<String, String>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a map of header names to single string values, each name given once")
}
fn visit_map<M: MapAccess<'de>>(self, mut access: M) -> Result<Self::Value, M::Error> {
let mut seen: Vec<(String, String)> =
Vec::with_capacity(access.size_hint().unwrap_or(0).min(1024));
while let Some((name, value)) = access.next_entry::<String, String>()? {
if let Some((existing, _)) =
seen.iter().find(|(k, _)| k.eq_ignore_ascii_case(&name))
{
return Err(M::Error::custom(format!(
"header `{name}` is already given as `{existing}`; a single-valued header \
object names each header once"
)));
}
seen.push((name, value));
}
Ok(seen.into_iter().collect())
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<HashMap<String, String>, D::Error> {
deserializer.deserialize_map(OneEachVisitor)
}
}
use serde::Deserialize;
fn parse_status_code_value<E: serde::de::Error>(value: serde_json::Value) -> Result<u16, E> {
match value {
serde_json::Value::Number(n) => n
.as_u64()
.and_then(|n| u16::try_from(n).ok())
.ok_or_else(|| E::custom("invalid status code number")),
serde_json::Value::String(s) => s
.parse::<u16>()
.map_err(|_| E::custom(format!("invalid status code string: {s}"))),
_ => Err(E::custom("statusCode must be a number or string")),
}
}
pub fn deserialize_status_code<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: serde::Deserializer<'de>,
{
parse_status_code_value(serde_json::Value::deserialize(deserializer)?)
}
pub fn deserialize_optional_status_code<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where
D: serde::Deserializer<'de>,
{
match serde_json::Value::deserialize(deserializer)? {
serde_json::Value::Null => Ok(None),
value => parse_status_code_value(value).map(Some),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Deserialize)]
struct HeadersIn {
#[serde(default, deserialize_with = "multi_value_headers::deserialize")]
headers: HashMap<String, Vec<String>>,
}
#[derive(Serialize)]
struct HeadersOut {
#[serde(serialize_with = "multi_value_headers::serialize")]
headers: HashMap<String, Vec<String>>,
}
#[derive(Deserialize)]
struct StatusIn {
#[serde(deserialize_with = "deserialize_status_code")]
status: u16,
}
#[derive(Deserialize)]
struct OptionalStatusIn {
#[serde(default, deserialize_with = "deserialize_optional_status_code")]
status: Option<u16>,
}
#[test]
fn headers_accept_a_bare_string_or_an_array() {
let one: HeadersIn = serde_json::from_str(r#"{"headers":{"X-One":"v"}}"#).unwrap();
assert_eq!(one.headers["X-One"], vec!["v".to_string()]);
let many: HeadersIn =
serde_json::from_str(r#"{"headers":{"Set-Cookie":["a","b"]}}"#).unwrap();
assert_eq!(
many.headers["Set-Cookie"],
vec!["a".to_string(), "b".to_string()]
);
}
#[test]
fn headers_coerce_numeric_and_bool_scalars() {
let r: HeadersIn = serde_json::from_str(
r#"{"headers":{"Content-Length":124,"X-Flag":true,"X-Ratio":1.5,"X-Multi":[200,"x",false]}}"#,
)
.expect("numeric/bool header values must be accepted (mb parity)");
assert_eq!(r.headers["Content-Length"], vec!["124".to_string()]);
assert_eq!(r.headers["X-Flag"], vec!["true".to_string()]);
assert_eq!(r.headers["X-Ratio"], vec!["1.5".to_string()]);
assert_eq!(
r.headers["X-Multi"],
vec!["200".to_string(), "x".to_string(), "false".to_string()]
);
}
#[test]
fn headers_merge_case_variant_keys_under_the_first_spelling() {
let r: HeadersIn =
serde_json::from_str(r#"{"headers":{"content-type":"a","Content-Type":"b"}}"#)
.expect("case-variant keys are a valid document, not an error");
assert_eq!(
r.headers.len(),
1,
"two spellings of one name is one header"
);
assert_eq!(
r.headers["content-type"],
vec!["a".to_string(), "b".to_string()],
"first spelling survives; values follow document order"
);
assert!(
!r.headers.contains_key("Content-Type"),
"the later spelling must not survive as a second key"
);
}
#[test]
fn headers_merge_byte_identical_duplicate_keys() {
let r: HeadersIn = serde_json::from_str(r#"{"headers":{"X-Dup":"a","X-Dup":"b"}}"#)
.expect("a repeated key is accepted");
assert_eq!(
r.headers["X-Dup"],
vec!["a".to_string(), "b".to_string()],
"a repeated key keeps both values instead of serde's silent last-wins"
);
}
#[test]
fn headers_preserve_a_lone_keys_spelling_byte_exact() {
let r: HeadersIn = serde_json::from_str(r#"{"headers":{"Content-type":"x"}}"#).unwrap();
assert_eq!(r.headers["Content-type"], vec!["x".to_string()]);
assert!(!r.headers.contains_key("Content-Type"));
assert!(!r.headers.contains_key("content-type"));
let shouty: HeadersIn =
serde_json::from_str(r#"{"headers":{"CONTENT-TYPE":"y"}}"#).unwrap();
assert_eq!(shouty.headers["CONTENT-TYPE"], vec!["y".to_string()]);
let served = serde_json::to_value(HeadersOut { headers: r.headers }).expect("serialize");
assert_eq!(served["headers"], serde_json::json!({"Content-type": "x"}));
}
#[test]
fn headers_merge_three_way_variants_with_mixed_scalars_and_arrays() {
let r: HeadersIn =
serde_json::from_str(r#"{"headers":{"X-A":1,"x-a":[true,"z"],"X-a":2.5}}"#)
.expect("#754 scalar coercion still applies to every merged entry");
assert_eq!(r.headers.len(), 1);
assert_eq!(
r.headers["X-A"],
vec![
"1".to_string(),
"true".to_string(),
"z".to_string(),
"2.5".to_string()
]
);
}
#[test]
fn headers_merge_is_deterministic_across_repeated_parses() {
for _ in 0..200 {
let r: HeadersIn = serde_json::from_str(
r#"{"headers":{"content-type":"a","Content-Type":"b","CONTENT-TYPE":"c"}}"#,
)
.unwrap();
assert_eq!(r.headers.len(), 1);
assert_eq!(
r.headers["content-type"],
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
}
#[test]
fn headers_fold_deterministically_from_a_value_too_even_though_the_order_differs() {
const TEXT: &str = r#"{"headers":{"set-cookie":"a","Set-Cookie":"b"}}"#;
let from_text: HeadersIn = serde_json::from_str(TEXT).unwrap();
assert_eq!(
from_text.headers["set-cookie"],
vec!["a".to_string(), "b".to_string()],
"streaming from text keeps document order, so the first spelling written wins"
);
let value: serde_json::Value = serde_json::from_str(TEXT).unwrap();
let from_value: HeadersIn = serde_json::from_value(value).unwrap();
assert_eq!(from_value.headers.len(), 1, "still one header either way");
assert_eq!(
from_value.headers["Set-Cookie"],
vec!["b".to_string(), "a".to_string()],
"a `Map` is key-sorted, so `Set-Cookie` (0x53) precedes `set-cookie` (0x73)"
);
}
#[test]
fn serde_json_parses_floats_correctly_rounded() {
for literal in [
"7e23",
"1e-23",
"1.23e-30",
"1.2299999999999999e-30",
"0.10018513143495411",
] {
let value: serde_json::Value = serde_json::from_str(literal).unwrap();
assert_eq!(value.to_string(), literal);
}
}
#[test]
fn headers_fold_only_ascii_case_and_keep_empty_shapes() {
let empty: HeadersIn = serde_json::from_str(r#"{"headers":{}}"#).unwrap();
assert!(empty.headers.is_empty());
let no_values: HeadersIn = serde_json::from_str(r#"{"headers":{"X-None":[]}}"#).unwrap();
assert_eq!(no_values.headers["X-None"], Vec::<String>::new());
let non_ascii: HeadersIn =
serde_json::from_str(r#"{"headers":{"X-Kä":"1","X-KÄ":"2"}}"#).unwrap();
assert_eq!(non_ascii.headers.len(), 2);
}
#[test]
fn headers_serialize_single_as_string_many_as_array_and_omit_empty() {
let out = HeadersOut {
headers: HashMap::from([
("X-One".to_string(), vec!["v".to_string()]),
(
"Set-Cookie".to_string(),
vec!["a".to_string(), "b".to_string()],
),
("X-Empty".to_string(), vec![]),
]),
};
let v = serde_json::to_value(&out).unwrap();
assert_eq!(v["headers"]["X-One"], serde_json::json!("v"));
assert_eq!(v["headers"]["Set-Cookie"], serde_json::json!(["a", "b"]));
assert!(
v["headers"].get("X-Empty").is_none(),
"a key with no values emits no header line, so it is omitted"
);
}
#[derive(Deserialize, Debug)]
struct SingleIn {
#[serde(default, deserialize_with = "single_value_headers::deserialize")]
headers: HashMap<String, String>,
}
#[test]
fn a_name_given_twice_in_different_case_is_rejected() {
let err = serde_json::from_str::<SingleIn>(
r#"{"headers":{"content-type":"a","Content-Type":"b"}}"#,
)
.expect_err("a single-valued header object names each header once");
let msg = err.to_string();
assert!(
msg.contains("content-type") && msg.contains("Content-Type"),
"the error must name BOTH spellings so the author can find them: {msg}"
);
}
#[test]
fn a_case_variant_name_is_rejected_through_a_value_too() {
let value: serde_json::Value =
serde_json::from_str(r#"{"headers":{"x-id":"a","X-Id":"b"}}"#).unwrap();
assert!(
serde_json::from_value::<SingleIn>(value).is_err(),
"routing through a Value must not launder a duplicate past the check"
);
}
#[test]
fn a_byte_identical_duplicate_is_rejected_on_the_text_path() {
assert!(
serde_json::from_str::<SingleIn>(r#"{"headers":{"X-Id":"a","X-Id":"b"}}"#).is_err(),
"serde's default map visitor would have silently kept the last one"
);
}
#[test]
fn an_ordinary_single_valued_header_object_is_unchanged() {
let ok: SingleIn =
serde_json::from_str(r#"{"headers":{"X-Id":"a","Content-Type":"text/plain"}}"#)
.expect("every name given once");
assert_eq!(ok.headers["X-Id"], "a");
assert_eq!(ok.headers["Content-Type"], "text/plain");
assert_eq!(ok.headers.len(), 2);
let empty: SingleIn = serde_json::from_str(r#"{"headers":{}}"#).unwrap();
assert!(empty.headers.is_empty());
let missing: SingleIn = serde_json::from_str("{}").unwrap();
assert!(missing.headers.is_empty());
}
#[test]
fn names_differing_outside_ascii_are_not_treated_as_duplicates() {
let ok: SingleIn =
serde_json::from_str(r#"{"headers":{"X-Kä":"1","X-KÄ":"2"}}"#).expect("distinct names");
assert_eq!(ok.headers.len(), 2);
}
#[test]
fn status_code_accepts_a_number_or_a_numeric_string() {
assert_eq!(
serde_json::from_str::<StatusIn>(r#"{"status":404}"#)
.unwrap()
.status,
404
);
assert_eq!(
serde_json::from_str::<StatusIn>(r#"{"status":"404"}"#)
.unwrap()
.status,
404
);
}
#[test]
fn status_code_rejects_junk_rather_than_defaulting() {
for junk in [
r#"{"status":"abc"}"#,
r#"{"status":true}"#,
r#"{"status":65536}"#,
r#"{"status":"65536"}"#,
r#"{"status":-1}"#,
r#"{"status":200.5}"#,
] {
assert!(
serde_json::from_str::<StatusIn>(junk).is_err(),
"{junk} must be an error, not a silent default"
);
}
assert_eq!(
serde_json::from_str::<StatusIn>(r#"{"status":65535}"#)
.unwrap()
.status,
65535
);
}
#[test]
fn optional_status_code_treats_null_and_absent_as_none() {
assert_eq!(
serde_json::from_str::<OptionalStatusIn>(r#"{}"#)
.unwrap()
.status,
None
);
assert_eq!(
serde_json::from_str::<OptionalStatusIn>(r#"{"status":null}"#)
.unwrap()
.status,
None
);
assert_eq!(
serde_json::from_str::<OptionalStatusIn>(r#"{"status":"201"}"#)
.unwrap()
.status,
Some(201)
);
}
}