From cc3a5857631be10cb06f06f190494fe493e13409 Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Wed, 12 Aug 2026 11:50:04 +0200 Subject: [PATCH] Support all whitespace characters in header values This PR is part of a series of PRs across several crates, to eventually fix web-platform-test failures for Servo. Servo currently fails [`/content-security-policy/generic/only-valid-whitespaces-are-allowed.html`](https://wpt.fyi/results/content-security-policy/generic/only-valid-whitespaces-are-allowed.html?product=servo) which is a test that checks whether various whitespace characters are properly parsed in the `Content-Security-Policy` header. Currently, Servo times out on this test, since it attempts to send a HTTP request with Hyper. Hyper calls `httparse` to parse the response returned by the test server with, which includes the `Content-Security-Policy` with the special whitespace character. When `httparse` attempts to parse this, it fails since it contains characters it currently does not accept. While RFC7230 (HTTP) does not allow such characters to be present, for web browsers [section 3.5](https://datatracker.ietf.org/doc/html/rfc7230#section-3.5) is relevant. That section states that for historical reasons (which is the case for web browsers), both `%x0B` and `%x0C` MAY be treated as regular space characters. Since currently `httparse` rejects parsing, Servo is unable to implement that requirement. To fix this issue, the following changes are made: 1. In `httparse`, support parsing `%x0B` and `%x0C` in header values 2. In `http`, consider `%x0B` and `%x0C` as opaque bytes for byte constructors 3. In `hyper`, use `HeaderValue::from_bytes` rather than the generic `HeaderValue::from_maybe_shared_unchecked` to operate on the raw bytes 4. In `content-security-policy` update policy parsing to check if directive value is an ASCII string. It already handles `%x0B` and `%x0C` in `is_char_ascii_whitespace`, but with the HTTP changes it exposed this other missing check 5. In Servo, replace `header.to_str()` with `str::from_utf8(header.as_bytes())` --- src/header/value.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/header/value.rs b/src/header/value.rs index 97bb49ef..59e65ae3 100644 --- a/src/header/value.rs +++ b/src/header/value.rs @@ -131,8 +131,8 @@ impl HeaderValue { /// Attempt to convert a byte slice to a `HeaderValue`. /// /// If the argument contains invalid header value bytes, an error is - /// returned. Only byte values between 32 and 255 (inclusive) are permitted, - /// excluding byte 127 (DEL). + /// returned. Only byte values 12, 13, and values between 32 and 255 (inclusive) + /// are permitted, excluding byte 127 (DEL). /// /// This function is intended to be replaced in the future by a `TryFrom` /// implementation once the trait is stabilized in std. @@ -570,7 +570,7 @@ const fn is_valid_ascii(b: u8) -> bool { // `HeaderValue::to_str`. #[inline] fn is_valid_ascii_or_opaque_byte(b: u8) -> bool { - b >= 32 && b != 127 || b == b'\t' + b >= 32 && b != 127 || b == b'\t' || b == b'\x0B' || b == b'\x0C' } impl fmt::Debug for InvalidHeaderValue { @@ -778,6 +778,8 @@ fn test_string_constructors_reject_non_ascii() { #[test] fn test_byte_constructors_allow_opaque_bytes_but_reject_del() { assert!(HeaderValue::from_bytes(b"hello\xff").is_ok()); + assert!(HeaderValue::from_bytes(b"hello\x0B").is_ok()); + assert!(HeaderValue::from_bytes(b"hello\x0C").is_ok()); assert!(HeaderValue::try_from(&b"hello\xff"[..]).is_ok()); assert!(HeaderValue::try_from(b"hello\xff".to_vec()).is_ok());