CVE-2024-23644: Interpretation Conflict (CWE-436)
value: Trillium is a composable toolkit for building internet applications with async rust. In `trillium-http` prior to 0.3.12 and `trillium-client` prior to 0.5.4, insufficient validation of outbound header values may lead to request splitting or response splitting attacks in scenarios where attackers have sufficient control over headers. This only affects use cases where attackers have control of request headers, and can insert "\r\n" sequences. Specifically, if untrusted and unvalidated input is inserted into header names or values.

Outbound `trillium_http::HeaderValue` and `trillium_http::HeaderName` can be constructed infallibly and were not checked for illegal bytes when sending requests from the client or responses from the server. Thus, if an attacker has sufficient control over header values (or names) in a request or response that they could inject `\r\n` sequences, they could get the client and server out of sync, and then pivot to gain control over other parts of requests or responses. (i.e. exfiltrating data from other requests, SSRF, etc.)

In `trillium-http` versions 0.3.12 and later, if a header name is invalid in server response headers, the specific header and any associated values are omitted from network transmission. Additionally, if a header value is invalid in server response headers, the individual header value is omitted from network transmission. Other headers values with the same header name will still be sent. In `trillium-client` versions 0.5.4 and later, if any header name or header value is invalid in the client request headers, awaiting the client Conn returns an `Error::MalformedHeader` prior to any network access. As a workaround, Trillium services and client applications should sanitize or validate untrusted input that is included in header values and header names. Carriage return, newline, and null characters are not allowed.
CWE description: Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.
CVSS v3 base score: 8.1
Published: 2024-01-24T20:15Z
Repository: https://github.com/trillium-rs/trillium
Language: Rust
Files changed: client/src/conn.rs, client/tests/unsafe_headers.rs, http/src/conn.rs, http/src/headers/header_name.rs, http/src/headers/header_value.rs, http/src/headers/unknown_header_name.rs, http/tests/unsafe_headers.rs
Commit date: 2024-01-23 16:48:50 -0800
Commit message: fix(security): handling of unsafe characters in outbound header names and values

Diff: diff --git a/client/src/conn.rs b/client/src/conn.rs
index 0b8c3b1..c541684 100644
--- a/client/src/conn.rs
+++ b/client/src/conn.rs
@@ -597,13 +597,24 @@ impl Conn {
             }
         }
 
         write!(buf, " HTTP/1.1\r\n")?;
 
-        for (header, values) in self.request_headers.iter() {
-            for value in values.iter() {
-                write!(buf, "{header}: {value}\r\n")?;
+        for (name, values) in &self.request_headers {
+            if !name.is_valid() {
+                return Err(Error::MalformedHeader(name.to_string().into()));
+            }
+
+            for value in values {
+                if !value.is_valid() {
+                    return Err(Error::MalformedHeader(
+                        format!("value for {name}: {value:?}").into(),
+                    ));
+                }
+                write!(buf, "{name}: ")?;
+                buf.extend_from_slice(value.as_ref());
+                write!(buf, "\r\n")?;
             }
         }
 
         write!(buf, "\r\n")?;
         log::trace!(
diff --git a/client/tests/unsafe_headers.rs b/client/tests/unsafe_headers.rs
new file mode 100644
index 0000000..0ac74f5
--- /dev/null
+++ b/client/tests/unsafe_headers.rs
@@ -0,0 +1,24 @@
+use test_harness::test;
+use trillium_client::{Client, KnownHeaderName};
+use trillium_testing::{connector, harness};
+
+#[test(harness)]
+async fn bad_characters_in_header_value() {
+    assert!(Client::new(connector(()))
+        .get("http://example.com")
+        .with_header(
+            KnownHeaderName::Referer,
+            "x\r\nConnection: keep-alive\r\n\r\nGET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
+        )
+        .await
+        .is_err());
+}
+
+#[test(harness)]
+async fn bad_characters_in_header_name() {
+    assert!(Client::new(connector(()))
+        .get("http://example.com")
+        .with_header("dnt: 1\r\nConnection", "keep-alive")
+        .await
+        .is_err());
+}
diff --git a/http/src/conn.rs b/http/src/conn.rs
index a5828f7..2ccbcc6 100644
--- a/http/src/conn.rs
+++ b/http/src/conn.rs
@@ -778,11 +778,11 @@ where
             Some(ref body) => body.len(),
             None => Some(0),
         }
     }
 
-    fn write_headers(&mut self, output_buffer: &mut Vec<u8>) -> std::io::Result<()> {
+    fn write_headers(&mut self, output_buffer: &mut Vec<u8>) -> Result<()> {
         use std::io::Write;
         let status = self.status().unwrap_or(Status::NotFound);
 
         write!(
             output_buffer,
@@ -799,15 +799,23 @@ where
             self.version,
             status,
             &self.response_headers
         );
 
-        for (header, values) in &self.response_headers {
-            for value in values {
-                write!(output_buffer, "{header}: ")?;
-                output_buffer.extend_from_slice(value.as_ref());
-                write!(output_buffer, "\r\n")?;
+        for (name, values) in &self.response_headers {
+            if name.is_valid() {
+                for value in values {
+                    if value.is_valid() {
+                        write!(output_buffer, "{name}: ")?;
+                        output_buffer.extend_from_slice(value.as_ref());
+                        write!(output_buffer, "\r\n")?;
+                    } else {
+                        log::error!("skipping invalid header value {value:?} for header {name}");
+                    }
+                }
+            } else {
+                log::error!("skipping invalid header with name {name:?}");
             }
         }
 
         write!(output_buffer, "\r\n")?;
         Ok(())
diff --git a/http/src/headers/header_name.rs b/http/src/headers/header_name.rs
index 9885726..5fe3e37 100644
--- a/http/src/headers/header_name.rs
+++ b/http/src/headers/header_name.rs
@@ -1,14 +1,14 @@
-use smartcow::SmartCow;
-use smartstring::alias::String as SmartString;
 use std::{
     fmt::{self, Debug, Display, Formatter},
     hash::Hash,
     str::FromStr,
 };
 
 use super::{KnownHeaderName, UnknownHeaderName};
+use crate::Error;
+use HeaderNameInner::{KnownHeader, UnknownHeader};
 
 /// The name of a http header. This can be either a
 /// [`KnownHeaderName`] or a string representation of an unknown
 /// header.
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -28,23 +28,19 @@ impl serde::Serialize for HeaderName<'_> {
 pub(super) enum HeaderNameInner<'a> {
     /// A `KnownHeaderName`
     KnownHeader(KnownHeaderName),
     UnknownHeader(UnknownHeaderName<'a>),
 }
-use crate::Error;
-use HeaderNameInner::{KnownHeader, UnknownHeader};
 
 impl<'a> HeaderName<'a> {
     /// Convert a potentially-borrowed headername to a static
     /// headername _by value_.
     #[must_use]
     pub fn into_owned(self) -> HeaderName<'static> {
         HeaderName(match self.0 {
             KnownHeader(known) => KnownHeader(known),
-            UnknownHeader(UnknownHeaderName(smartcow)) => {
-                UnknownHeader(UnknownHeaderName(smartcow.into_owned()))
-            }
+            UnknownHeader(uhn) => UnknownHeader(uhn.into_owned()),
         })
     }
 
     /// Convert a potentially-borrowed headername to a static
     /// headername _by cloning if needed from a borrow_. If you have
@@ -53,10 +49,18 @@ impl<'a> HeaderName<'a> {
     /// `self.clone().into_owned()`.
     #[must_use]
     pub fn to_owned(&self) -> HeaderName<'static> {
         self.clone().into_owned()
     }
+
+    /// Determine if this header name contains only the appropriate characters
+    pub fn is_valid(&self) -> bool {
+        match &self.0 {
+            KnownHeader(_) => true,
+            UnknownHeader(uh) => uh.is_valid(),
+        }
+    }
 }
 
 impl PartialEq<KnownHeaderName> for HeaderName<'_> {
     fn eq(&self, other: &KnownHeaderName) -> bool {
         match &self.0 {
@@ -77,33 +81,34 @@ impl PartialEq<KnownHeaderName> for &HeaderName<'_> {
 
 impl From<String> for HeaderName<'static> {
     fn from(s: String) -> Self {
         Self(match s.parse::<KnownHeaderName>() {
             Ok(khn) => KnownHeader(khn),
-            Err(()) => UnknownHeader(UnknownHeaderName(SmartCow::Owned(s.into()))),
+            Err(()) => UnknownHeader(UnknownHeaderName::from(s)),
         })
     }
 }
 
 impl<'a> From<&'a str> for HeaderName<'a> {
     fn from(s: &'a str) -> Self {
         Self(match s.parse::<KnownHeaderName>() {
             Ok(khn) => KnownHeader(khn),
-            Err(_e) => UnknownHeader(UnknownHeaderName(SmartCow::Borrowed(s))),
+            Err(_e) => UnknownHeader(UnknownHeaderName::from(s)),
         })
     }
 }
 
 impl FromStr for HeaderName<'static> {
     type Err = Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        if s.is_ascii() {
-            Ok(Self(match s.parse::<KnownHeaderName>() {
-                Ok(known) => KnownHeader(known),
-                Err(()) => UnknownHeader(UnknownHeaderName(SmartCow::Owned(SmartString::from(s)))),
-            }))
+        if let Ok(known) = s.parse::<KnownHeaderName>() {
+            return Ok(known.into());
+        }
+        let uhn = UnknownHeaderName::from(s.to_string());
+        if uhn.is_valid() {
+            Ok(uhn.into())
         } else {
             Err(Error::MalformedHeader(s.to_string().into()))
         }
     }
 }
diff --git a/http/src/headers/header_value.rs b/http/src/headers/header_value.rs
index c657f6e..3fbf6de 100644
--- a/http/src/headers/header_value.rs
+++ b/http/src/headers/header_value.rs
@@ -1,18 +1,25 @@
 use smallvec::SmallVec;
 use smartcow::SmartCow;
-
 use std::{
     borrow::Cow,
     fmt::{Debug, Display, Formatter},
 };
+use HeaderValueInner::{Bytes, Utf8};
 
 /// A `HeaderValue` represents the right hand side of a single `name:
 /// value` pair.
 #[derive(Eq, PartialEq, Clone)]
 pub struct HeaderValue(HeaderValueInner);
 
+impl HeaderValue {
+    /// determine if this header contains no unsafe characters (\r, \n, \0)
+    pub fn is_valid(&self) -> bool {
+        memchr::memchr3(b'\r', b'\n', 0, self.as_ref()).is_none()
+    }
+}
+
 #[derive(Eq, PartialEq, Clone)]
 pub(crate) enum HeaderValueInner {
     Utf8(SmartCow<'static>),
     Bytes(SmallVec<[u8; 32]>),
 }
@@ -22,21 +29,21 @@ impl serde::Serialize for HeaderValue {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: serde::Serializer,
     {
         match &self.0 {
-            HeaderValueInner::Utf8(s) => serializer.serialize_str(s),
-            HeaderValueInner::Bytes(bytes) => serializer.serialize_bytes(bytes),
+            Utf8(s) => serializer.serialize_str(s),
+            Bytes(bytes) => serializer.serialize_bytes(bytes),
         }
     }
 }
 
 impl Debug for HeaderValue {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         match &self.0 {
-            HeaderValueInner::Utf8(s) => Debug::fmt(s, f),
-            HeaderValueInner::Bytes(b) => Debug::fmt(&String::from_utf8_lossy(b), f),
+            Utf8(s) => Debug::fmt(s, f),
+            Bytes(b) => Debug::fmt(&String::from_utf8_lossy(b), f),
         }
     }
 }
 
 impl HeaderValue {
@@ -45,12 +52,12 @@ impl HeaderValue {
     /// somehow, match directly on the `HeaderValue` as an enum and
     /// handle that case. If you need a byte slice regardless of
     /// whether it's utf8, use the `AsRef<[u8]>` impl
     pub fn as_str(&self) -> Option<&str> {
         match &self.0 {
-            HeaderValueInner::Utf8(utf8) => Some(utf8),
-            HeaderValueInner::Bytes(_) => None,
+            Utf8(utf8) => Some(utf8),
+            Bytes(_) => None,
         }
     }
 
     pub(crate) fn as_lower(&self) -> Option<SmartCow<'_>> {
         self.as_str().map(|s| {
@@ -64,57 +71,57 @@ impl HeaderValue {
 }
 
 impl Display for HeaderValue {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         match &self.0 {
-            HeaderValueInner::Utf8(s) => f.write_str(s),
-            HeaderValueInner::Bytes(b) => f.write_str(&String::from_utf8_lossy(b)),
+            Utf8(s) => f.write_str(s),
+            Bytes(b) => f.write_str(&String::from_utf8_lossy(b)),
         }
     }
 }
 
 impl From<Vec<u8>> for HeaderValue {
     fn from(v: Vec<u8>) -> Self {
         match String::from_utf8(v) {
-            Ok(s) => Self(HeaderValueInner::Utf8(SmartCow::Owned(s.into()))),
-            Err(e) => Self(HeaderValueInner::Bytes(e.into_bytes().into())),
+            Ok(s) => Self(Utf8(SmartCow::Owned(s.into()))),
+            Err(e) => Self(Bytes(e.into_bytes().into())),
         }
     }
 }
 
 impl From<Cow<'static, str>> for HeaderValue {
     fn from(c: Cow<'static, str>) -> Self {
-        Self(HeaderValueInner::Utf8(SmartCow::from(c)))
+        Self(Utf8(SmartCow::from(c)))
     }
 }
 
 impl From<&'static [u8]> for HeaderValue {
     fn from(b: &'static [u8]) -> Self {
         match std::str::from_utf8(b) {
-            Ok(s) => Self(HeaderValueInner::Utf8(SmartCow::Borrowed(s))),
-            Err(_) => Self(HeaderValueInner::Bytes(b.into())),
+            Ok(s) => Self(Utf8(SmartCow::Borrowed(s))),
+            Err(_) => Self(Bytes(b.into())),
         }
     }
 }
 
 impl From<String> for HeaderValue {
     fn from(s: String) -> Self {
-        Self(HeaderValueInner::Utf8(SmartCow::Owned(s.into())))
+        Self(Utf8(SmartCow::Owned(s.into())))
     }
 }
 
 impl From<&'static str> for HeaderValue {
     fn from(s: &'static str) -> Self {
-        Self(HeaderValueInner::Utf8(SmartCow::Borrowed(s)))
+        Self(Utf8(SmartCow::Borrowed(s)))
     }
 }
 
 impl AsRef<[u8]> for HeaderValue {
     fn as_ref(&self) -> &[u8] {
         match &self.0 {
-            HeaderValueInner::Utf8(utf8) => utf8.as_bytes(),
-            HeaderValueInner::Bytes(b) => b,
+            Utf8(utf8) => utf8.as_bytes(),
+            Bytes(b) => b,
         }
     }
 }
 
 impl PartialEq<&str> for HeaderValue {
diff --git a/http/src/headers/unknown_header_name.rs b/http/src/headers/unknown_header_name.rs
index 0376180..21031ad 100644
--- a/http/src/headers/unknown_header_name.rs
+++ b/http/src/headers/unknown_header_name.rs
@@ -1,18 +1,16 @@
+use super::{HeaderName, HeaderNameInner::UnknownHeader};
+use hashbrown::Equivalent;
+use smartcow::SmartCow;
 use std::{
     fmt::{self, Debug, Display, Formatter},
     hash::{Hash, Hasher},
     ops::Deref,
 };
 
-use hashbrown::Equivalent;
-use smartcow::SmartCow;
-
-use super::{HeaderName, HeaderNameInner::UnknownHeader};
-
 #[derive(Clone)]
-pub(super) struct UnknownHeaderName<'a>(pub(super) SmartCow<'a>);
+pub(super) struct UnknownHeaderName<'a>(SmartCow<'a>);
 
 impl PartialEq for UnknownHeaderName<'_> {
     fn eq(&self, other: &Self) -> bool {
         self.0.eq_ignore_ascii_case(&other.0)
     }
@@ -44,10 +42,34 @@ impl<'a> From<UnknownHeaderName<'a>> for HeaderName<'a> {
     fn from(value: UnknownHeaderName<'a>) -> Self {
         HeaderName(UnknownHeader(value))
     }
 }
 
+impl UnknownHeaderName<'_> {
+    pub(crate) fn is_valid(&self) -> bool {
+        self.0
+            .chars()
+            .all(|c| matches!(c, 'a'..='z'|'A'..='Z'|'0'..='9'|'-'|'_'))
+    }
+
+    pub(crate) fn into_owned(self) -> UnknownHeaderName<'static> {
+        UnknownHeaderName(self.0.into_owned())
+    }
+}
+
+impl From<String> for UnknownHeaderName<'static> {
+    fn from(value: String) -> Self {
+        Self(value.into())
+    }
+}
+
+impl<'a> From<&'a str> for UnknownHeaderName<'a> {
+    fn from(value: &'a str) -> Self {
+        Self(value.into())
+    }
+}
+
 impl<'a> From<SmartCow<'a>> for UnknownHeaderName<'a> {
     fn from(value: SmartCow<'a>) -> Self {
         Self(value)
     }
 }
diff --git a/http/tests/unsafe_headers.rs b/http/tests/unsafe_headers.rs
new file mode 100644
index 0000000..eea1e8a
--- /dev/null
+++ b/http/tests/unsafe_headers.rs
@@ -0,0 +1,49 @@
+use indoc::{formatdoc, indoc};
+use pretty_assertions::assert_eq;
+use stopper::Stopper;
+use test_harness::test;
+use trillium_http::{Conn, KnownHeaderName, SERVER};
+use trillium_testing::{harness, TestResult, TestTransport};
+
+const TEST_DATE: &str = "Tue, 21 Nov 2023 21:27:21 GMT";
+
+async fn handler(mut conn: Conn<TestTransport>) -> Conn<TestTransport> {
+    conn.set_status(200);
+    conn.set_response_body("response: 0123456789");
+    conn.response_headers_mut()
+        .insert(KnownHeaderName::Date, TEST_DATE);
+    conn.response_headers_mut().insert(
+        KnownHeaderName::Connection,
+        "close\r\nGET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
+    );
+    conn.response_headers_mut().insert("Bad\r\nHeader", "true");
+    conn
+}
+
+#[test(harness)]
+async fn bad_headers() -> TestResult {
+    let (client, server) = TestTransport::new();
+
+    trillium_testing::spawn(async move {
+        Conn::map(server, Stopper::new(), handler).await.unwrap();
+    });
+
+    client.write_all(indoc! {"
+        GET / HTTP/1.1\r
+        Host: example.com\r
+        \r
+    "});
+
+    let expected_response = formatdoc! {"
+        HTTP/1.1 200 OK\r
+        Server: {SERVER}\r
+        Date: {TEST_DATE}\r
+        Content-Length: 20\r
+        \r
+        response: 0123456789\
+    "};
+
+    assert_eq!(client.read_available_string().await, expected_response);
+
+    Ok(())
+}

Vulnerable code:         for (header, values) in self.request_headers.iter() {
            for value in values.iter() {
                write!(buf, "{header}: {value}\r\n")?;
    fn write_headers(&mut self, output_buffer: &mut Vec<u8>) -> std::io::Result<()> {
        for (header, values) in &self.response_headers {
            for value in values {
                write!(output_buffer, "{header}: ")?;
                output_buffer.extend_from_slice(value.as_ref());
                write!(output_buffer, "\r\n")?;
use smartcow::SmartCow;
use smartstring::alias::String as SmartString;
use crate::Error;
use HeaderNameInner::{KnownHeader, UnknownHeader};
            UnknownHeader(UnknownHeaderName(smartcow)) => {
                UnknownHeader(UnknownHeaderName(smartcow.into_owned()))
            }
            Err(()) => UnknownHeader(UnknownHeaderName(SmartCow::Owned(s.into()))),
            Err(_e) => UnknownHeader(UnknownHeaderName(SmartCow::Borrowed(s))),
        if s.is_ascii() {
            Ok(Self(match s.parse::<KnownHeaderName>() {
                Ok(known) => KnownHeader(known),
                Err(()) => UnknownHeader(UnknownHeaderName(SmartCow::Owned(SmartString::from(s)))),
            }))

            HeaderValueInner::Utf8(s) => serializer.serialize_str(s),
            HeaderValueInner::Bytes(bytes) => serializer.serialize_bytes(bytes),
            HeaderValueInner::Utf8(s) => Debug::fmt(s, f),
            HeaderValueInner::Bytes(b) => Debug::fmt(&String::from_utf8_lossy(b), f),
            HeaderValueInner::Utf8(utf8) => Some(utf8),
            HeaderValueInner::Bytes(_) => None,
            HeaderValueInner::Utf8(s) => f.write_str(s),
            HeaderValueInner::Bytes(b) => f.write_str(&String::from_utf8_lossy(b)),
            Ok(s) => Self(HeaderValueInner::Utf8(SmartCow::Owned(s.into()))),
            Err(e) => Self(HeaderValueInner::Bytes(e.into_bytes().into())),
        Self(HeaderValueInner::Utf8(SmartCow::from(c)))
            Ok(s) => Self(HeaderValueInner::Utf8(SmartCow::Borrowed(s))),
            Err(_) => Self(HeaderValueInner::Bytes(b.into())),
        Self(HeaderValueInner::Utf8(SmartCow::Owned(s.into())))
        Self(HeaderValueInner::Utf8(SmartCow::Borrowed(s)))
            HeaderValueInner::Utf8(utf8) => utf8.as_bytes(),
            HeaderValueInner::Bytes(b) => b,
use hashbrown::Equivalent;
use smartcow::SmartCow;

use super::{HeaderName, HeaderNameInner::UnknownHeader};

pub(super) struct UnknownHeaderName<'a>(pub(super) SmartCow<'a>);
Fixed code:         for (name, values) in &self.request_headers {
            if !name.is_valid() {
                return Err(Error::MalformedHeader(name.to_string().into()));
            }

            for value in values {
                if !value.is_valid() {
                    return Err(Error::MalformedHeader(
                        format!("value for {name}: {value:?}").into(),
                    ));
                }
                write!(buf, "{name}: ")?;
                buf.extend_from_slice(value.as_ref());
                write!(buf, "\r\n")?;
use test_harness::test;
use trillium_client::{Client, KnownHeaderName};
use trillium_testing::{connector, harness};

#[test(harness)]
async fn bad_characters_in_header_value() {
    assert!(Client::new(connector(()))
        .get("http://example.com")
        .with_header(
            KnownHeaderName::Referer,
            "x\r\nConnection: keep-alive\r\n\r\nGET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
        )
        .await
        .is_err());
}

#[test(harness)]
async fn bad_characters_in_header_name() {
    assert!(Client::new(connector(()))
        .get("http://example.com")
        .with_header("dnt: 1\r\nConnection", "keep-alive")
        .await
        .is_err());
}
    fn write_headers(&mut self, output_buffer: &mut Vec<u8>) -> Result<()> {
        for (name, values) in &self.response_headers {
            if name.is_valid() {
                for value in values {
                    if value.is_valid() {
                        write!(output_buffer, "{name}: ")?;
                        output_buffer.extend_from_slice(value.as_ref());
                        write!(output_buffer, "\r\n")?;
                    } else {
                        log::error!("skipping invalid header value {value:?} for header {name}");
                    }
                }
            } else {
                log::error!("skipping invalid header with name {name:?}");
use crate::Error;
use HeaderNameInner::{KnownHeader, UnknownHeader};
            UnknownHeader(uhn) => UnknownHeader(uhn.into_owned()),

    /// Determine if this header name contains only the appropriate characters
    pub fn is_valid(&self) -> bool {
        match &self.0 {
            KnownHeader(_) => true,
            UnknownHeader(uh) => uh.is_valid(),
        }
    }
            Err(()) => UnknownHeader(UnknownHeaderName::from(s)),
            Err(_e) => UnknownHeader(UnknownHeaderName::from(s)),
        if let Ok(known) = s.parse::<KnownHeaderName>() {
            return Ok(known.into());
        }
        let uhn = UnknownHeaderName::from(s.to_string());
        if uhn.is_valid() {
            Ok(uhn.into())
use HeaderValueInner::{Bytes, Utf8};
impl HeaderValue {
    /// determine if this header contains no unsafe characters (\r, \n, \0)
    pub fn is_valid(&self) -> bool {
        memchr::memchr3(b'\r', b'\n', 0, self.as_ref()).is_none()
    }
}

            Utf8(s) => serializer.serialize_str(s),
            Bytes(bytes) => serializer.serialize_bytes(bytes),
            Utf8(s) => Debug::fmt(s, f),
            Bytes(b) => Debug::fmt(&String::from_utf8_lossy(b), f),
            Utf8(utf8) => Some(utf8),
            Bytes(_) => None,
            Utf8(s) => f.write_str(s),
            Bytes(b) => f.write_str(&String::from_utf8_lossy(b)),
            Ok(s) => Self(Utf8(SmartCow::Owned(s.into()))),
            Err(e) => Self(Bytes(e.into_bytes().into())),
        Self(Utf8(SmartCow::from(c)))
            Ok(s) => Self(Utf8(SmartCow::Borrowed(s))),
            Err(_) => Self(Bytes(b.into())),
        Self(Utf8(SmartCow::Owned(s.into())))
        Self(Utf8(SmartCow::Borrowed(s)))
            Utf8(utf8) => utf8.as_bytes(),
            Bytes(b) => b,
use super::{HeaderName, HeaderNameInner::UnknownHeader};
use hashbrown::Equivalent;
use smartcow::SmartCow;
pub(super) struct UnknownHeaderName<'a>(SmartCow<'a>);
impl UnknownHeaderName<'_> {
    pub(crate) fn is_valid(&self) -> bool {
        self.0
            .chars()
            .all(|c| matches!(c, 'a'..='z'|'A'..='Z'|'0'..='9'|'-'|'_'))
    }

    pub(crate) fn into_owned(self) -> UnknownHeaderName<'static> {
        UnknownHeaderName(self.0.into_owned())
    }
}

impl From<String> for UnknownHeaderName<'static> {
    fn from(value: String) -> Self {
        Self(value.into())
    }
}

impl<'a> From<&'a str> for UnknownHeaderName<'a> {
    fn from(value: &'a str) -> Self {
        Self(value.into())
    }
}

use indoc::{formatdoc, indoc};
use pretty_assertions::assert_eq;
use stopper::Stopper;
use test_harness::test;
use trillium_http::{Conn, KnownHeaderName, SERVER};
use trillium_testing::{harness, TestResult, TestTransport};

const TEST_DATE: &str = "Tue, 21 Nov 2023 21:27:21 GMT";

async fn handler(mut conn: Conn<TestTransport>) -> Conn<TestTransport> {
    conn.set_status(200);
    conn.set_response_body("response: 0123456789");
    conn.response_headers_mut()
        .insert(KnownHeaderName::Date, TEST_DATE);
    conn.response_headers_mut().insert(
        KnownHeaderName::Connection,
        "close\r\nGET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
    );
    conn.response_headers_mut().insert("Bad\r\nHeader", "true");
    conn
}

#[test(harness)]
async fn bad_headers() -> TestResult {
    let (client, server) = TestTransport::new();

    trillium_testing::spawn(async move {
        Conn::map(server, Stopper::new(), handler).await.unwrap();
    });

    client.write_all(indoc! {"
        GET / HTTP/1.1\r
        Host: example.com\r
        \r
    "});

    let expected_response = formatdoc! {"
        HTTP/1.1 200 OK\r
        Server: {SERVER}\r
        Date: {TEST_DATE}\r
        Content-Length: 20\r
        \r
        response: 0123456789\
    "};

    assert_eq!(client.read_available_string().await, expected_response);

    Ok(())
}