Skip to main content

pingora_http/
lib.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! HTTP header objects that preserve http header cases
16//!
17//! Although HTTP header names are supposed to be case-insensitive for compatibility, proxies
18//! ideally shouldn't alter the HTTP traffic, especially the headers they don't need to read.
19//!
20//! This crate provide structs and methods to preserve the headers in order to build a transparent
21//! proxy.
22
23#![allow(clippy::new_without_default)]
24
25use bytes::BufMut;
26use http::header::{AsHeaderName, HeaderName, HeaderValue};
27use http::request::Builder as ReqBuilder;
28use http::request::Parts as ReqParts;
29use http::response::Builder as RespBuilder;
30use http::response::Parts as RespParts;
31use http::uri::Uri;
32use pingora_error::{ErrorType::*, OrErr, Result};
33use std::borrow::Cow;
34use std::ops::Deref;
35
36pub use http::method::Method;
37pub use http::status::StatusCode;
38pub use http::version::Version;
39pub use http::HeaderMap as HMap;
40
41pub mod authority;
42use authority::{raw_target_authority, RawTargetAuthority};
43
44mod case_header_name;
45use case_header_name::CaseHeaderName;
46pub use case_header_name::IntoCaseHeaderName;
47
48pub mod prelude {
49    pub use crate::RequestHeader;
50    pub use crate::ResponseHeader;
51}
52
53/* an ordered header map to store the original case of each header name
54HMap({
55    "foo": ["Foo", "foO", "FoO"]
56})
57The order how HeaderMap iter over its items is "arbitrary, but consistent".
58Hopefully this property makes sure this map of header names always iterates in the
59same order of the map of header values.
60This idea is inspaired by hyper @nox
61*/
62type CaseMap = HMap<CaseHeaderName>;
63
64pub enum HeaderNameVariant<'a> {
65    Case(&'a CaseHeaderName),
66    Titled(&'a str),
67}
68
69/// The HTTP request header type.
70///
71/// This type is similar to [http::request::Parts] but preserves header name case.
72/// It also preserves request path even if it is not UTF-8.
73///
74/// [RequestHeader] implements [Deref] for [http::request::Parts] so it can be used as it in most
75/// places. Mutable access to the underlying parts is intentionally not provided because header and
76/// URI mutations must use methods on [RequestHeader] to preserve its internal state.
77///
78/// ```compile_fail
79/// use pingora_http::RequestHeader;
80///
81/// let mut request = RequestHeader::build("GET", b"/", None).unwrap();
82/// request.headers.remove("user-agent");
83/// ```
84#[derive(Debug)]
85pub struct RequestHeader {
86    base: ReqParts,
87    header_name_map: Option<CaseMap>,
88    raw_target: RawTarget,
89    // whether we send END_STREAM with HEADERS for h2 requests
90    send_end_stream: bool,
91}
92
93/// How a request-target is stored, and how [`RequestHeader::raw_path`] recovers it.
94///
95/// Whether the target round-trips through the URI and whether it is valid UTF-8 are two
96/// separate questions with only three valid combinations, so they are one value rather
97/// than two fields that could disagree.
98#[derive(Debug, Clone, PartialEq, Eq)]
99enum RawTarget {
100    /// The URI round-trips the target, so no separate copy is kept: origin-form,
101    /// asterisk-form and query-only targets come back out of it byte-identical, and the
102    /// empty target, having no bytes to reproduce, resolves to "/".
103    FromUri,
104    /// The target is kept verbatim for the wire rather than recovered from the URI, because
105    /// the URI is not guaranteed to reproduce it: absolute-form contributes only its path
106    /// component. Covers every non-origin-form target, including the authority-form CONNECT
107    /// target and opaque or unclassifiable ones, whose bytes the URI happens to hold whole.
108    Verbatim(Box<[u8]>),
109    /// As [`Self::Verbatim`], but the target is not valid UTF-8, so the URI holds a lossy
110    /// rendering of it and [`RequestHeader::raw_path_is_utf8`] reports false.
111    Lossy(Box<[u8]>),
112}
113
114impl RawTarget {
115    /// The stored bytes, or `None` when the URI is the source of truth.
116    fn bytes(&self) -> Option<&[u8]> {
117        match self {
118            Self::FromUri => None,
119            Self::Verbatim(target) | Self::Lossy(target) => Some(target.as_ref()),
120        }
121    }
122
123    /// Whether the target is valid UTF-8, i.e. the URI is not a lossy rendering of it.
124    ///
125    /// Matched exhaustively: a new variant must state its encoding rather than inherit a
126    /// default, because HTTP/2 egress refuses to forward a target this reports as lossy.
127    fn is_utf8(&self) -> bool {
128        match self {
129            Self::FromUri | Self::Verbatim(_) => true,
130            Self::Lossy(_) => false,
131        }
132    }
133}
134
135impl AsRef<ReqParts> for RequestHeader {
136    fn as_ref(&self) -> &ReqParts {
137        &self.base
138    }
139}
140
141impl Deref for RequestHeader {
142    type Target = ReqParts;
143
144    fn deref(&self) -> &Self::Target {
145        &self.base
146    }
147}
148
149impl RequestHeader {
150    fn new_no_case(size_hint: Option<usize>) -> Self {
151        let mut base = ReqBuilder::new().body(()).unwrap().into_parts().0;
152        base.headers.reserve(http_header_map_upper_bound(size_hint));
153        RequestHeader {
154            base,
155            header_name_map: None,
156            raw_target: RawTarget::FromUri,
157            send_end_stream: true,
158        }
159    }
160
161    /// Create a new [RequestHeader] with the given method and path.
162    ///
163    /// The `path` can be non UTF-8.
164    pub fn build(
165        method: impl TryInto<Method>,
166        path: &[u8],
167        size_hint: Option<usize>,
168    ) -> Result<Self> {
169        let mut req = Self::build_no_case(method, path, size_hint)?;
170        req.header_name_map = Some(CaseMap::with_capacity(http_header_map_upper_bound(
171            size_hint,
172        )));
173        Ok(req)
174    }
175
176    /// Create a new [RequestHeader] with the given method and path without preserving header case.
177    ///
178    /// A [RequestHeader] created from this type is more space efficient than those from [Self::build()].
179    ///
180    /// Use this method if reading from or writing to HTTP/2 sessions where header case doesn't matter anyway.
181    pub fn build_no_case(
182        method: impl TryInto<Method>,
183        path: &[u8],
184        size_hint: Option<usize>,
185    ) -> Result<Self> {
186        let mut req = Self::new_no_case(size_hint);
187        req.base.method = method
188            .try_into()
189            .explain_err(InvalidHTTPHeader, |_| "invalid method")?;
190        req.set_raw_path(path)?;
191        Ok(req)
192    }
193
194    /// Append the header name and value to `self`.
195    ///
196    /// If there are already some headers under the same name, a new value will be added without
197    /// any others being removed.
198    pub fn append_header(
199        &mut self,
200        name: impl IntoCaseHeaderName,
201        value: impl TryInto<HeaderValue>,
202    ) -> Result<bool> {
203        let header_value = value
204            .try_into()
205            .explain_err(InvalidHTTPHeader, |_| "invalid value while append")?;
206        append_header_value(
207            self.header_name_map.as_mut(),
208            &mut self.base.headers,
209            name,
210            header_value,
211        )
212    }
213
214    /// Insert the header name and value to `self`.
215    ///
216    /// Different from [Self::append_header()], this method will replace all other existing headers
217    /// under the same name (case-insensitive).
218    pub fn insert_header(
219        &mut self,
220        name: impl IntoCaseHeaderName,
221        value: impl TryInto<HeaderValue>,
222    ) -> Result<()> {
223        let header_value = value
224            .try_into()
225            .explain_err(InvalidHTTPHeader, |_| "invalid value while insert")?;
226        insert_header_value(
227            self.header_name_map.as_mut(),
228            &mut self.base.headers,
229            name,
230            header_value,
231        )
232    }
233
234    /// Remove all headers under the name
235    pub fn remove_header<'a, N: ?Sized>(&mut self, name: &'a N) -> Option<HeaderValue>
236    where
237        &'a N: 'a + AsHeaderName,
238    {
239        remove_header(self.header_name_map.as_mut(), &mut self.base.headers, name)
240    }
241
242    /// Write the header to the `buf` in HTTP/1.1 wire format.
243    ///
244    /// The header case will be preserved.
245    pub fn header_to_h1_wire(&self, buf: &mut impl BufMut) {
246        header_to_h1_wire(self.header_name_map.as_ref(), &self.base.headers, buf)
247    }
248
249    /// If case sensitivity is enabled, returns an iterator to iterate over case-sensitive header names and values.
250    /// Otherwise returns an empty iterator.
251    ///
252    /// Headers of the same name are visited in insertion order.
253    pub fn case_header_iter(&self) -> impl Iterator<Item = (&CaseHeaderName, &HeaderValue)> + '_ {
254        case_header_iter(self.header_name_map.as_ref(), &self.base.headers)
255    }
256
257    /// Returns true if the request has case-sensitive headers.
258    pub fn has_case(&self) -> bool {
259        self.header_name_map.is_some()
260    }
261
262    pub fn map<F: FnMut(HeaderNameVariant, &HeaderValue) -> Result<()>>(
263        &self,
264        mut f: F,
265    ) -> Result<()> {
266        let key_map = self.header_name_map.as_ref();
267        let value_map = &self.base.headers;
268
269        if let Some(key_map) = key_map {
270            let iter = key_map.iter().zip(value_map.iter());
271            for ((header, case_header), (header2, val)) in iter {
272                if header != header2 {
273                    // in case the header iteration order changes in future versions of HMap
274                    panic!("header iter mismatch {}, {}", header, header2)
275                }
276                f(HeaderNameVariant::Case(case_header), val)?;
277            }
278        } else {
279            for (header, value) in value_map {
280                let titled_header =
281                    case_header_name::titled_header_name_str(header).unwrap_or(header.as_str());
282                f(HeaderNameVariant::Titled(titled_header), value)?;
283            }
284        }
285
286        Ok(())
287    }
288
289    /// Return mutable access to the request extensions.
290    pub fn extensions_mut(&mut self) -> &mut http::Extensions {
291        &mut self.base.extensions
292    }
293
294    /// Set the request method
295    pub fn set_method(&mut self, method: Method) {
296        self.base.method = method;
297    }
298
299    /// Set the request URI
300    pub fn set_uri(&mut self, uri: http::Uri) {
301        self.base.uri = uri;
302        // The Uri is now the sole source of the target, so drop any stored bytes: they
303        // would otherwise be used when serializing.
304        self.raw_target = RawTarget::FromUri;
305    }
306
307    /// Set the request target directly via raw bytes.
308    ///
309    /// Generally prefer [`Self::set_uri()`] to modify the header's URI if able.
310    ///
311    /// This API is to allow supporting non UTF-8 cases, and request-targets that are not
312    /// in origin-form ([RFC 9112 section 3.2]).
313    ///
314    /// Origin-form and asterisk-form targets round-trip through the URI. Absolute-form and
315    /// the authority-form CONNECT target do not, so they are additionally kept verbatim for
316    /// [`Self::raw_path()`]. For those two forms the URI carries only the path component,
317    /// which means [`http::Uri::path()`] returns a path rather than a whole URL and
318    /// [`http::Uri::authority()`] is left unset.
319    ///
320    /// Any fragment is dropped: it is not part of the request-target and must not be sent
321    /// upstream.
322    ///
323    /// [RFC 9112 section 3.2]: https://www.rfc-editor.org/rfc/rfc9112.html#section-3.2
324    pub fn set_raw_path(&mut self, path: &[u8]) -> Result<()> {
325        // Everything is computed before anything is stored: a rejected target must
326        // leave the existing one intact.
327        let parsed = parse_request_target(path)?;
328        self.base.uri = parsed.uri;
329        // Origin-form and asterisk-form store RawTarget::FromUri, so a reused header
330        // (e.g. a CONNECT mutated into a normal request) cannot serialize a stale
331        // target.
332        self.raw_target = parsed.raw_target;
333        Ok(())
334    }
335
336    /// Set whether we send an END_STREAM on H2 request HEADERS if body is empty.
337    pub fn set_send_end_stream(&mut self, send_end_stream: bool) {
338        self.send_end_stream = send_end_stream;
339    }
340
341    /// Returns if we support sending an END_STREAM on H2 request HEADERS if body is empty,
342    /// returns None if not H2.
343    pub fn send_end_stream(&self) -> Option<bool> {
344        if self.base.version != Version::HTTP_2 {
345            return None;
346        }
347        Some(self.send_end_stream)
348    }
349
350    /// Return the request target in its raw format, as it should appear on the wire.
351    ///
352    /// For origin-form and asterisk-form this is the path and query. For absolute-form and
353    /// the authority-form CONNECT target it is the whole target as received, less any
354    /// fragment.
355    ///
356    /// Non-UTF8 is supported; [`Self::raw_path_is_utf8()`] reports whether these bytes are
357    /// valid UTF-8 or were replaced lossily in the URI.
358    pub fn raw_path(&self) -> &[u8] {
359        self.raw_target.bytes().unwrap_or_else(|| {
360            self.base
361                .uri
362                .path_and_query()
363                .map(|path| path.as_str().as_bytes())
364                .or_else(|| {
365                    self.base
366                        .uri
367                        .authority()
368                        .map(|authority| authority.as_str().as_bytes())
369                })
370                .unwrap_or_default()
371        })
372    }
373
374    /// Whether [`Self::raw_path`] is valid UTF-8 without lossy replacement.
375    pub fn raw_path_is_utf8(&self) -> bool {
376        self.raw_target.is_utf8()
377    }
378
379    /// Return the file extension of the path
380    pub fn uri_file_extension(&self) -> Option<&str> {
381        // get everything after the last '.' in path
382        let (_, ext) = self
383            .uri
384            .path_and_query()
385            .and_then(|pq| pq.path().rsplit_once('.'))?;
386        Some(ext)
387    }
388
389    /// Set http version
390    pub fn set_version(&mut self, version: Version) {
391        self.base.version = version;
392    }
393
394    /// Clone `self` into [http::request::Parts].
395    ///
396    /// [ReqParts] has nowhere to keep a request-target that does not round-trip through the
397    /// URI, so an absolute-form or CONNECT target does not survive the conversion:
398    /// rebuilding a [RequestHeader] from the result serializes the URI's origin-form path
399    /// instead of the original bytes. [Clone] keeps it; [Self::set_raw_path()] restores it.
400    pub fn as_owned_parts(&self) -> ReqParts {
401        clone_req_parts(&self.base)
402    }
403}
404
405impl Clone for RequestHeader {
406    fn clone(&self) -> Self {
407        Self {
408            base: self.as_owned_parts(),
409            header_name_map: self.header_name_map.clone(),
410            raw_target: self.raw_target.clone(),
411            send_end_stream: self.send_end_stream,
412        }
413    }
414}
415
416/// Header case is not recovered, because [ReqParts] keeps none, and neither is a
417/// request-target that does not round-trip through the URI: the target is taken from the
418/// URI rather than the absolute-form or CONNECT bytes a [RequestHeader] preserves. Set one
419/// with [Self::set_raw_path()].
420impl From<ReqParts> for RequestHeader {
421    fn from(parts: ReqParts) -> RequestHeader {
422        Self {
423            base: parts,
424            header_name_map: None,
425            // The Uri is the only target available here, so it is the one that serializes.
426            raw_target: RawTarget::FromUri,
427            send_end_stream: true,
428        }
429    }
430}
431
432impl From<RequestHeader> for ReqParts {
433    fn from(resp: RequestHeader) -> ReqParts {
434        resp.base
435    }
436}
437
438/// The HTTP response header type.
439///
440/// This type is similar to [http::response::Parts] but preserves header name case.
441/// [ResponseHeader] implements [Deref] for [http::response::Parts] so it can be used as it in most
442/// places. Mutable access to the underlying parts is intentionally not provided because header
443/// mutations must use methods on [ResponseHeader] to preserve its internal state.
444///
445/// ```compile_fail
446/// use pingora_http::ResponseHeader;
447///
448/// let mut response = ResponseHeader::build(200, None).unwrap();
449/// response.headers.remove("server");
450/// ```
451#[derive(Debug)]
452pub struct ResponseHeader {
453    base: RespParts,
454    // an ordered header map to store the original case of each header name
455    header_name_map: Option<CaseMap>,
456    // the reason phrase of the response, if unset, a default one will be used
457    reason_phrase: Option<String>,
458}
459
460impl AsRef<RespParts> for ResponseHeader {
461    fn as_ref(&self) -> &RespParts {
462        &self.base
463    }
464}
465
466impl Deref for ResponseHeader {
467    type Target = RespParts;
468
469    fn deref(&self) -> &Self::Target {
470        &self.base
471    }
472}
473
474impl Clone for ResponseHeader {
475    fn clone(&self) -> Self {
476        Self {
477            base: self.as_owned_parts(),
478            header_name_map: self.header_name_map.clone(),
479            reason_phrase: self.reason_phrase.clone(),
480        }
481    }
482}
483
484// The `ResponseHeader` will be the no case variant, because `RespParts` keeps no header case
485impl From<RespParts> for ResponseHeader {
486    fn from(parts: RespParts) -> ResponseHeader {
487        Self {
488            base: parts,
489            header_name_map: None,
490            reason_phrase: None,
491        }
492    }
493}
494
495impl From<ResponseHeader> for RespParts {
496    fn from(resp: ResponseHeader) -> RespParts {
497        resp.base
498    }
499}
500
501impl From<Box<ResponseHeader>> for Box<RespParts> {
502    fn from(resp: Box<ResponseHeader>) -> Box<RespParts> {
503        Box::new(resp.base)
504    }
505}
506
507impl ResponseHeader {
508    fn new(size_hint: Option<usize>) -> Self {
509        let mut resp_header = Self::new_no_case(size_hint);
510        resp_header.header_name_map = Some(CaseMap::with_capacity(http_header_map_upper_bound(
511            size_hint,
512        )));
513        resp_header
514    }
515
516    fn new_no_case(size_hint: Option<usize>) -> Self {
517        let mut base = RespBuilder::new().body(()).unwrap().into_parts().0;
518        base.headers.reserve(http_header_map_upper_bound(size_hint));
519        ResponseHeader {
520            base,
521            header_name_map: None,
522            reason_phrase: None,
523        }
524    }
525
526    /// Create a new [ResponseHeader] with the given status code.
527    pub fn build(code: impl TryInto<StatusCode>, size_hint: Option<usize>) -> Result<Self> {
528        let mut resp = Self::new(size_hint);
529        resp.base.status = code
530            .try_into()
531            .explain_err(InvalidHTTPHeader, |_| "invalid status")?;
532        Ok(resp)
533    }
534
535    /// Create a new [ResponseHeader] with the given status code without preserving header case.
536    ///
537    /// A [ResponseHeader] created from this type is more space efficient than those from [Self::build()].
538    ///
539    /// Use this method if reading from or writing to HTTP/2 sessions where header case doesn't matter anyway.
540    pub fn build_no_case(code: impl TryInto<StatusCode>, size_hint: Option<usize>) -> Result<Self> {
541        let mut resp = Self::new_no_case(size_hint);
542        resp.base.status = code
543            .try_into()
544            .explain_err(InvalidHTTPHeader, |_| "invalid status")?;
545        Ok(resp)
546    }
547
548    /// Append the header name and value to `self`.
549    ///
550    /// If there are already some headers under the same name, a new value will be added without
551    /// any others being removed.
552    pub fn append_header(
553        &mut self,
554        name: impl IntoCaseHeaderName,
555        value: impl TryInto<HeaderValue>,
556    ) -> Result<bool> {
557        let header_value = value
558            .try_into()
559            .explain_err(InvalidHTTPHeader, |_| "invalid value while append")?;
560        append_header_value(
561            self.header_name_map.as_mut(),
562            &mut self.base.headers,
563            name,
564            header_value,
565        )
566    }
567
568    /// Insert the header name and value to `self`.
569    ///
570    /// Different from [Self::append_header()], this method will replace all other existing headers
571    /// under the same name (case insensitive).
572    pub fn insert_header(
573        &mut self,
574        name: impl IntoCaseHeaderName,
575        value: impl TryInto<HeaderValue>,
576    ) -> Result<()> {
577        let header_value = value
578            .try_into()
579            .explain_err(InvalidHTTPHeader, |_| "invalid value while insert")?;
580        insert_header_value(
581            self.header_name_map.as_mut(),
582            &mut self.base.headers,
583            name,
584            header_value,
585        )
586    }
587
588    /// Remove all headers under the name
589    pub fn remove_header<'a, N: ?Sized>(&mut self, name: &'a N) -> Option<HeaderValue>
590    where
591        &'a N: 'a + AsHeaderName,
592    {
593        remove_header(self.header_name_map.as_mut(), &mut self.base.headers, name)
594    }
595
596    /// Write the header to the `buf` in HTTP/1.1 wire format.
597    ///
598    /// The header case will be preserved.
599    pub fn header_to_h1_wire(&self, buf: &mut impl BufMut) {
600        header_to_h1_wire(self.header_name_map.as_ref(), &self.base.headers, buf)
601    }
602
603    /// If case sensitivity is enabled, returns an iterator to iterate over case-sensitive header names and values.
604    /// Otherwise returns an empty iterator.
605    ///
606    /// Headers of the same name are visited in insertion order.
607    pub fn case_header_iter(&self) -> impl Iterator<Item = (&CaseHeaderName, &HeaderValue)> + '_ {
608        case_header_iter(self.header_name_map.as_ref(), &self.base.headers)
609    }
610
611    /// Returns true if the response has case-sensitive headers.
612    pub fn has_case(&self) -> bool {
613        self.header_name_map.is_some()
614    }
615
616    pub fn map<F: FnMut(HeaderNameVariant, &HeaderValue) -> Result<()>>(
617        &self,
618        mut f: F,
619    ) -> Result<()> {
620        let key_map = self.header_name_map.as_ref();
621        let value_map = &self.base.headers;
622
623        if let Some(key_map) = key_map {
624            let iter = key_map.iter().zip(value_map.iter());
625            for ((header, case_header), (header2, val)) in iter {
626                if header != header2 {
627                    // in case the header iteration order changes in future versions of HMap
628                    panic!("header iter mismatch {}, {}", header, header2)
629                }
630                f(HeaderNameVariant::Case(case_header), val)?;
631            }
632        } else {
633            for (header, value) in value_map {
634                let titled_header =
635                    case_header_name::titled_header_name_str(header).unwrap_or(header.as_str());
636                f(HeaderNameVariant::Titled(titled_header), value)?;
637            }
638        }
639
640        Ok(())
641    }
642
643    /// Return mutable access to the response extensions.
644    pub fn extensions_mut(&mut self) -> &mut http::Extensions {
645        &mut self.base.extensions
646    }
647
648    /// Set the status code
649    pub fn set_status(&mut self, status: impl TryInto<StatusCode>) -> Result<()> {
650        self.base.status = status
651            .try_into()
652            .explain_err(InvalidHTTPHeader, |_| "invalid status")?;
653        Ok(())
654    }
655
656    /// Set the HTTP version
657    pub fn set_version(&mut self, version: Version) {
658        self.base.version = version
659    }
660
661    /// Set the HTTP reason phase. If `None`, a default reason phase will be used
662    pub fn set_reason_phrase(&mut self, reason_phrase: Option<&str>) -> Result<()> {
663        // No need to allocate memory to store the phrase if it is the default one.
664        if reason_phrase == self.base.status.canonical_reason() {
665            self.reason_phrase = None;
666            return Ok(());
667        }
668
669        // TODO: validate it "*( HTAB / SP / VCHAR / obs-text )"
670        self.reason_phrase = reason_phrase.map(str::to_string);
671        Ok(())
672    }
673
674    /// Get the HTTP reason phase. If [Self::set_reason_phrase()] is never called
675    /// or set to `None`, a default reason phase will be used
676    pub fn get_reason_phrase(&self) -> Option<&str> {
677        self.reason_phrase
678            .as_deref()
679            .or_else(|| self.base.status.canonical_reason())
680    }
681
682    /// Clone `self` into [http::response::Parts].
683    pub fn as_owned_parts(&self) -> RespParts {
684        clone_resp_parts(&self.base)
685    }
686
687    /// Helper function to set the HTTP content length on the response header.
688    pub fn set_content_length(&mut self, len: usize) -> Result<()> {
689        self.insert_header(http::header::CONTENT_LENGTH, len)
690    }
691}
692
693/// Build a [Uri] carrying only a path-and-query component. `target` is the original
694/// request-target, used for error context.
695fn path_and_query_uri(path_and_query: &str, target: &str) -> Result<Uri> {
696    Uri::builder()
697        .path_and_query(path_and_query)
698        .build()
699        .explain_err(InvalidHTTPHeader, |_| format!("invalid uri {target}"))
700}
701
702/// A request-target parsed into the pieces a [RequestHeader] stores.
703struct ParsedRequestTarget {
704    /// The [Uri] to store, carrying at most a path-and-query component.
705    uri: Uri,
706    /// How the target is recovered for the wire.
707    raw_target: RawTarget,
708}
709
710/// Parse a request-target (RFC 9112 §3.2) into the pieces to store on the header.
711fn parse_request_target(target: &[u8]) -> Result<ParsedRequestTarget> {
712    // A fragment is not part of the request-target (§3.2) and is separated from the URI
713    // before dereference (RFC 3986 §3.5), so it must not reach the upstream request-line.
714    // `#` is ASCII, so stripping it here rather than after the UTF-8 check applies the
715    // same rule to targets that are not valid UTF-8, which are forwarded verbatim. Both
716    // authority classifiers terminate at `#` as well, so dropping it cannot change the
717    // authority they reconcile against `Host`.
718    let target = match target.iter().position(|&byte| byte == b'#') {
719        Some(fragment_start) => &target[..fragment_start],
720        None => target,
721    };
722
723    // Forms the Uri round-trips on its own, so they need no separate copy: origin-form
724    // (§3.2.1), asterisk-form (§3.2.4) and query-only targets all come back out of the
725    // Uri byte-identical, so anything reaching the wire from them is unchanged. A target
726    // the fragment strip left empty is the one exception: it has no bytes to reproduce,
727    // and path_and_query() renders it as "/".
728    if target.is_empty() || matches!(target.first(), Some(b'/' | b'?')) || target == b"*" {
729        return Ok(match std::str::from_utf8(target) {
730            Ok(target) => ParsedRequestTarget {
731                uri: path_and_query_uri(target, target)?,
732                raw_target: RawTarget::FromUri,
733            },
734            // Put a valid UTF-8 rendering into the Uri for read-only access, and keep the
735            // original bytes for the wire.
736            Err(_) => {
737                let lossy = String::from_utf8_lossy(target);
738                ParsedRequestTarget {
739                    uri: path_and_query_uri(&lossy, &lossy)?,
740                    raw_target: RawTarget::Lossy(target.into()),
741                }
742            }
743        });
744    }
745
746    // Absolute-form (§3.2.2) and the authority-form CONNECT target (§3.2.3) are kept
747    // verbatim for raw_path(). Which bytes reach the Uri depends on the form: an
748    // absolute-form target contributes only its path component, so callers reading
749    // uri.path() see a path rather than a whole URL, while a target that carries no
750    // absolute-form authority has no such component to isolate and reaches the Uri whole.
751    //
752    // Scheme and authority are deliberately left off the Uri. The proxy layer
753    // reconciles the target's authority against `Host` by parsing these raw bytes;
754    // populating uri.authority() here would send that reconciliation down its URI
755    // branch instead, rewriting the target on egress.
756    //
757    // For absolute-form, the authority boundary comes from the same classifier the
758    // proxy layer reconciles with, so the path extracted here cannot disagree with the
759    // authority validated there. A second parser with its own idea of where the
760    // authority ends would let targets like `foo:bar://host/admin` yield a path that
761    // was never validated.
762    // from_utf8_lossy allocates only to substitute replacement characters, so it borrows
763    // exactly when the target is already valid UTF-8.
764    let lossy_target = String::from_utf8_lossy(target);
765    let uri = match raw_target_authority(target) {
766        RawTargetAuthority::Absolute { path_and_query, .. } => {
767            // The classifier splits on ASCII delimiters only, so this is a suffix of the
768            // target and is lossy exactly when the target is.
769            let path_and_query = String::from_utf8_lossy(path_and_query);
770            match path_and_query.as_ref() {
771                "" => Uri::default(),
772                // "http://host?q=1" has no path, but origin-form requires at least "/"
773                // (§3.2.1), so anchor the component to the root.
774                pq if !pq.starts_with('/') => path_and_query_uri(&format!("/{pq}"), &lossy_target)?,
775                pq => path_and_query_uri(pq, &lossy_target)?,
776            }
777        }
778        // No absolute-form authority: authority-form CONNECT (§3.2.3), opaque custom
779        // schemes, and ambiguous targets. Storing these as path-and-query depends on how
780        // permissive the linked http crate is. Rejection prevents header construction,
781        // including every CONNECT, which has no other target form.
782        //
783        // Anchor the Uri to the root. `raw_target` preserves the bytes for the wire, while
784        // rooting prevents an unvalidated target fragment from becoming a path; see the
785        // security note above and the `://` cases in
786        // test_unclassifiable_targets_are_anchored_to_the_root.
787        RawTargetAuthority::None | RawTargetAuthority::AmbiguousAuthority => Uri::default(),
788    };
789    Ok(ParsedRequestTarget {
790        uri,
791        raw_target: match lossy_target {
792            Cow::Borrowed(_) => RawTarget::Verbatim(target.into()),
793            Cow::Owned(_) => RawTarget::Lossy(target.into()),
794        },
795    })
796}
797
798fn clone_req_parts(me: &ReqParts) -> ReqParts {
799    let mut parts = ReqBuilder::new()
800        .method(me.method.clone())
801        .uri(me.uri.clone())
802        .version(me.version)
803        .body(())
804        .unwrap()
805        .into_parts()
806        .0;
807    parts.headers = me.headers.clone();
808    parts.extensions = me.extensions.clone();
809    parts
810}
811
812fn clone_resp_parts(me: &RespParts) -> RespParts {
813    let mut parts = RespBuilder::new()
814        .status(me.status)
815        .version(me.version)
816        .body(())
817        .unwrap()
818        .into_parts()
819        .0;
820    parts.headers = me.headers.clone();
821    parts.extensions = me.extensions.clone();
822    parts
823}
824
825// This function returns an upper bound on the size of the header map used inside the http crate.
826// As of version 0.2, there is a limit of 1 << 15 (32,768) items inside the map. There is an
827// assertion against this size inside the crate, so we want to avoid panicking by not exceeding this
828// upper bound.
829fn http_header_map_upper_bound(size_hint: Option<usize>) -> usize {
830    // Even though the crate has 1 << 15 as the max size, calls to `with_capacity` invoke a
831    // function that returns the size + size / 3.
832    //
833    // See https://github.com/hyperium/http/blob/34a9d6bdab027948d6dea3b36d994f9cbaf96f75/src/header/map.rs#L3220
834    //
835    // Therefore we set our max size to be even lower, so we guarantee ourselves we won't hit that
836    // upper bound in the crate. Any way you cut it, 4,096 headers is insane.
837    const PINGORA_MAX_HEADER_COUNT: usize = 4096;
838    const INIT_HEADER_SIZE: usize = 8;
839
840    // We select the size hint or the max size here, ensuring that we pick a value substantially lower
841    // than 1 << 15 with room to grow the header map.
842    std::cmp::min(
843        size_hint.unwrap_or(INIT_HEADER_SIZE),
844        PINGORA_MAX_HEADER_COUNT,
845    )
846}
847
848#[inline]
849fn append_header_value<T>(
850    name_map: Option<&mut CaseMap>,
851    value_map: &mut HMap<T>,
852    name: impl IntoCaseHeaderName,
853    value: T,
854) -> Result<bool> {
855    let case_header_name = name.into_case_header_name();
856    let header_name: HeaderName = case_header_name
857        .as_slice()
858        .try_into()
859        .or_err(InvalidHTTPHeader, "invalid header name")?;
860    // store the original case in the map
861    if let Some(name_map) = name_map {
862        // Use the non-panicking `try_append`: the infallible `append` calls
863        // `.expect("size overflows MAX_SIZE")` internally, which would abort the
864        // process if the case map ever exceeded `http`'s `MAX_SIZE` (1 << 15).
865        name_map
866            .try_append(header_name.clone(), case_header_name)
867            .or_err(InvalidHTTPHeader, "header name map size overflows MAX_SIZE")?;
868    }
869
870    // Non-panicking `try_append` for the same reason as the case map above.
871    value_map.try_append(header_name, value).or_err(
872        InvalidHTTPHeader,
873        "header value map size overflows MAX_SIZE",
874    )
875}
876
877#[inline]
878fn insert_header_value<T>(
879    name_map: Option<&mut CaseMap>,
880    value_map: &mut HMap<T>,
881    name: impl IntoCaseHeaderName,
882    value: T,
883) -> Result<()> {
884    let case_header_name = name.into_case_header_name();
885    let header_name: HeaderName = case_header_name
886        .as_slice()
887        .try_into()
888        .or_err(InvalidHTTPHeader, "invalid header name")?;
889    if let Some(name_map) = name_map {
890        // store the original case in the map
891        name_map.insert(header_name.clone(), case_header_name);
892    }
893    value_map.insert(header_name, value);
894    Ok(())
895}
896
897// the &N here is to avoid clone(). None Copy type like String can impl AsHeaderName
898#[inline]
899fn remove_header<'a, T, N: ?Sized>(
900    name_map: Option<&mut CaseMap>,
901    value_map: &mut HMap<T>,
902    name: &'a N,
903) -> Option<T>
904where
905    &'a N: 'a + AsHeaderName,
906{
907    let removed = value_map.remove(name);
908    if removed.is_some() {
909        if let Some(name_map) = name_map {
910            name_map.remove(name);
911        }
912    }
913    removed
914}
915
916/// Build a [`HeaderValue`] from owned bytes, normalizing the value per RFC
917/// 9110 section 5.5 and RFC 9112 section 5.2: obs-fold continuations
918/// collapse to a single SP, and any stray CR / LF / NUL is replaced with SP.
919/// Zero-copy when the input contains no CR/LF/NUL.
920///
921/// # Precondition
922///
923/// Input must come from a conformant HTTP/1.1 parser: bytes must satisfy
924/// the `field-content` grammar (SP, HTAB, `%x21-7E`, `obs-text` `%x80-FF`),
925/// with CR/LF/NUL only appearing as part of an obs-fold or as invalid bytes
926/// that this function will replace with SP. All workspace callers satisfy
927/// this via httparse.
928///
929/// # Panics
930///
931/// Debug builds panic on precondition violation via the sanity check in
932/// [`HeaderValue::from_maybe_shared_unchecked`]. Release builds skip the
933/// check; invalid input is undefined behavior per the `http` crate.
934pub fn header_value_from_raw(raw: impl Into<bytes::Bytes>) -> HeaderValue {
935    let normalized = normalize_field_value(raw.into());
936    // SAFETY: `normalize_field_value` replaces all CR/LF/NUL with SP; by
937    // precondition the remaining bytes pass the `http` crate's `is_valid`
938    // byte-set check (other controls except HTAB are not expected here). The
939    // crate's documented safety contract names "valid UTF-8", but its
940    // internal use of the bytes never relies on UTF-8 in release. Matches
941    // long-standing precedent.
942    unsafe { HeaderValue::from_maybe_shared_unchecked(normalized) }
943}
944
945/// Build a [`HeaderValue`] from a borrowed slice, normalizing obs-fold per
946/// RFC 9112 section 5.2. The slice is copied into an owned buffer.
947///
948/// Precondition and panic behavior are the same as [`header_value_from_raw`].
949pub fn header_value_from_slice(raw: &[u8]) -> HeaderValue {
950    header_value_from_raw(bytes::Bytes::copy_from_slice(raw))
951}
952
953/// Normalize a header field value per RFC 9110 section 5.5 and RFC 9112
954/// section 5.2:
955///
956/// - Each CRLF + WSP obs-fold continuation collapses to a single SP
957///   ([RFC 9112 section 5.2]).
958/// - Any standalone CR, LF, or NUL (not part of an obs-fold being collapsed)
959///   is replaced with a single SP ([RFC 9110 section 5.5]).
960/// - Other CTL characters are retained, as RFC 9110 section 5.5 permits.
961///
962/// Zero-copy when the input contains no CR, LF, or NUL.
963/// Example: `b"obs\r\n fold\r\n\t line"` becomes `b"obs fold line"`.
964///
965/// [RFC 9112 section 5.2]: https://datatracker.ietf.org/doc/html/rfc9112#section-5.2
966/// [RFC 9110 section 5.5]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
967fn normalize_field_value(raw: bytes::Bytes) -> bytes::Bytes {
968    // Fast path: no CR/LF/NUL, nothing to do.
969    if !raw.iter().any(|b| matches!(b, b'\r' | b'\n' | b'\0')) {
970        return raw;
971    }
972
973    // No LF means no obs-fold (which requires CRLF). Replace each stray CR
974    // or NUL with SP per RFC 9110 section 5.5.
975    let Some(first_nl) = raw.iter().position(|b| *b == b'\n') else {
976        let replaced: Vec<u8> = raw
977            .iter()
978            .map(|&b| if matches!(b, b'\r' | b'\0') { b' ' } else { b })
979            .collect();
980        return bytes::Bytes::from(replaced);
981    };
982
983    // Mid-segment CRs (and NULs) — which boundary trimming can't reach because
984    // they're not at a segment boundary — are replaced with SP at copy time
985    // per RFC 9110 section 5.5. Empty continuations contribute no SP, so
986    // leading/trailing newlines don't introduce spurious whitespace.
987    fn push_with_replacement(dst: &mut Vec<u8>, src: &[u8]) {
988        dst.extend(
989            src.iter()
990                .map(|&b| if matches!(b, b'\r' | b'\0') { b' ' } else { b }),
991        );
992    }
993
994    // Trim ASCII whitespace at segment boundaries (we split on `\n`) so the
995    // trailing `\r` of each CRLF is absorbed into the obs-fold collapse along
996    // with the fold's SP/HTAB run.
997    let head = raw[..first_nl].trim_ascii_end();
998    let mut unfolded = Vec::with_capacity(raw.len());
999    push_with_replacement(&mut unfolded, head);
1000    for line in raw[first_nl + 1..].split(|b| *b == b'\n') {
1001        let line = line.trim_ascii();
1002        if line.is_empty() {
1003            continue;
1004        }
1005        if !unfolded.is_empty() {
1006            unfolded.push(b' ');
1007        }
1008        push_with_replacement(&mut unfolded, line);
1009    }
1010    bytes::Bytes::from(unfolded)
1011}
1012
1013#[inline]
1014fn header_to_h1_wire(key_map: Option<&CaseMap>, value_map: &HMap, buf: &mut impl BufMut) {
1015    const CRLF: &[u8; 2] = b"\r\n";
1016    const HEADER_KV_DELIMITER: &[u8; 2] = b": ";
1017
1018    if let Some(key_map) = key_map {
1019        case_header_iter(key_map.into(), value_map).for_each(|(case_header, val)| {
1020            buf.put_slice(case_header.as_slice());
1021            buf.put_slice(HEADER_KV_DELIMITER);
1022            buf.put_slice(val.as_ref());
1023            buf.put_slice(CRLF);
1024        });
1025    } else {
1026        for (header, value) in value_map {
1027            let titled_header =
1028                case_header_name::titled_header_name_str(header).unwrap_or(header.as_str());
1029            buf.put_slice(titled_header.as_bytes());
1030            buf.put_slice(HEADER_KV_DELIMITER);
1031            buf.put_slice(value.as_ref());
1032            buf.put_slice(CRLF);
1033        }
1034    }
1035}
1036
1037#[inline]
1038fn case_header_iter<'a>(
1039    name_map: Option<&'a CaseMap>,
1040    value_map: &'a HMap,
1041) -> impl Iterator<Item = (&'a CaseHeaderName, &'a HeaderValue)> + 'a {
1042    name_map.into_iter().flat_map(|name_map| {
1043        name_map
1044            .iter()
1045            .zip(value_map.iter())
1046            .map(|((h1, name), (h2, value))| {
1047                // in case the header iteration order changes in future versions of HMap
1048                assert_eq!(h1, h2, "header iter mismatch {}, {}", h1, h2);
1049                (name, value)
1050            })
1051    })
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056    use super::*;
1057
1058    #[test]
1059    fn header_map_upper_bound() {
1060        assert_eq!(8, http_header_map_upper_bound(None));
1061        assert_eq!(16, http_header_map_upper_bound(Some(16)));
1062        assert_eq!(4096, http_header_map_upper_bound(Some(7777)));
1063    }
1064
1065    #[test]
1066    fn test_single_header() {
1067        let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1068        req.insert_header("foo", "bar").unwrap();
1069        req.insert_header("FoO", "Bar").unwrap();
1070        let mut buf: Vec<u8> = vec![];
1071        req.header_to_h1_wire(&mut buf);
1072        assert_eq!(buf, b"FoO: Bar\r\n");
1073        req.case_header_iter().enumerate().for_each(|(i, (k, v))| {
1074            let name = String::from_utf8_lossy(k.as_slice()).into_owned();
1075            let value = String::from_utf8_lossy(v.as_ref()).into_owned();
1076            match i + 1 {
1077                1 => {
1078                    assert_eq!(name, "FoO");
1079                    assert_eq!(value, "Bar");
1080                }
1081                _ => panic!("too many headers"),
1082            }
1083        });
1084
1085        let mut resp = ResponseHeader::new(None);
1086        resp.insert_header("foo", "bar").unwrap();
1087        resp.insert_header("FoO", "Bar").unwrap();
1088        let mut buf: Vec<u8> = vec![];
1089        resp.header_to_h1_wire(&mut buf);
1090        assert_eq!(buf, b"FoO: Bar\r\n");
1091        resp.case_header_iter().enumerate().for_each(|(i, (k, v))| {
1092            let name = String::from_utf8_lossy(k.as_slice()).into_owned();
1093            let value = String::from_utf8_lossy(v.as_ref()).into_owned();
1094            match i + 1 {
1095                1 => {
1096                    assert_eq!(name, "FoO");
1097                    assert_eq!(value, "Bar");
1098                }
1099                _ => panic!("too many headers"),
1100            }
1101        });
1102    }
1103
1104    #[test]
1105    fn test_single_header_no_case() {
1106        let mut req = RequestHeader::new_no_case(None);
1107        req.insert_header("foo", "bar").unwrap();
1108        req.insert_header("FoO", "Bar").unwrap();
1109        let mut buf: Vec<u8> = vec![];
1110        req.header_to_h1_wire(&mut buf);
1111        assert_eq!(buf, b"foo: Bar\r\n");
1112        assert!(req.case_header_iter().next().is_none());
1113
1114        let mut resp = ResponseHeader::new_no_case(None);
1115        resp.insert_header("foo", "bar").unwrap();
1116        resp.insert_header("FoO", "Bar").unwrap();
1117        let mut buf: Vec<u8> = vec![];
1118        resp.header_to_h1_wire(&mut buf);
1119        assert_eq!(buf, b"foo: Bar\r\n");
1120        assert!(resp.case_header_iter().next().is_none());
1121    }
1122
1123    #[test]
1124    fn test_multiple_header() {
1125        let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1126        req.append_header("FoO", "Bar").unwrap();
1127        req.append_header("fOO", "bar").unwrap();
1128        req.append_header("BAZ", "baR").unwrap();
1129        req.append_header(http::header::CONTENT_LENGTH, "0")
1130            .unwrap();
1131        req.append_header("a", "b").unwrap();
1132        req.remove_header("a");
1133        let mut buf: Vec<u8> = vec![];
1134        req.header_to_h1_wire(&mut buf);
1135        assert_eq!(
1136            buf,
1137            b"FoO: Bar\r\nfOO: bar\r\nBAZ: baR\r\nContent-Length: 0\r\n"
1138        );
1139        req.case_header_iter().enumerate().for_each(|(i, (k, v))| {
1140            let name = String::from_utf8_lossy(k.as_slice()).into_owned();
1141            let value = String::from_utf8_lossy(v.as_ref()).into_owned();
1142            match i + 1 {
1143                1 => {
1144                    assert_eq!(name, "FoO");
1145                    assert_eq!(value, "Bar");
1146                }
1147                2 => {
1148                    assert_eq!(name, "fOO");
1149                    assert_eq!(value, "bar");
1150                }
1151                3 => {
1152                    assert_eq!(name, "BAZ");
1153                    assert_eq!(value, "baR");
1154                }
1155                4 => {
1156                    assert_eq!(name, "Content-Length");
1157                    assert_eq!(value, "0");
1158                }
1159                _ => panic!("too many headers"),
1160            }
1161        });
1162
1163        let mut resp = ResponseHeader::new(None);
1164        resp.append_header("FoO", "Bar").unwrap();
1165        resp.append_header("fOO", "bar").unwrap();
1166        resp.append_header("BAZ", "baR").unwrap();
1167        resp.append_header(http::header::CONTENT_LENGTH, "0")
1168            .unwrap();
1169        resp.append_header("a", "b").unwrap();
1170        resp.remove_header("a");
1171        let mut buf: Vec<u8> = vec![];
1172        resp.header_to_h1_wire(&mut buf);
1173        assert_eq!(
1174            buf,
1175            b"FoO: Bar\r\nfOO: bar\r\nBAZ: baR\r\nContent-Length: 0\r\n"
1176        );
1177        resp.case_header_iter().enumerate().for_each(|(i, (k, v))| {
1178            let name = String::from_utf8_lossy(k.as_slice()).into_owned();
1179            let value = String::from_utf8_lossy(v.as_ref()).into_owned();
1180            match i + 1 {
1181                1 => {
1182                    assert_eq!(name, "FoO");
1183                    assert_eq!(value, "Bar");
1184                }
1185                2 => {
1186                    assert_eq!(name, "fOO");
1187                    assert_eq!(value, "bar");
1188                }
1189                3 => {
1190                    assert_eq!(name, "BAZ");
1191                    assert_eq!(value, "baR");
1192                }
1193                4 => {
1194                    assert_eq!(name, "Content-Length");
1195                    assert_eq!(value, "0");
1196                }
1197                _ => panic!("too many headers"),
1198            }
1199        });
1200    }
1201
1202    // These two no longer need patched_http1: the target carries no representable
1203    // path-and-query, so the Uri is rooted without consulting the linked http crate.
1204    #[test]
1205    fn test_invalid_path() {
1206        let raw_path = b"Hello\xF0\x90\x80World";
1207        let req = RequestHeader::build("GET", &raw_path[..], None).unwrap();
1208        assert_eq!("/", req.uri.path_and_query().unwrap());
1209        assert_eq!(raw_path, req.raw_path());
1210        assert!(!req.raw_path_is_utf8());
1211    }
1212
1213    #[test]
1214    fn test_override_invalid_path() {
1215        let raw_path = b"Hello\xF0\x90\x80World";
1216        let mut req = RequestHeader::build("GET", &raw_path[..], None).unwrap();
1217        assert_eq!("/", req.uri.path_and_query().unwrap());
1218        assert_eq!(raw_path, req.raw_path());
1219
1220        let new_path = "/HelloWorld";
1221        req.set_uri(Uri::builder().path_and_query(new_path).build().unwrap());
1222        assert_eq!(new_path, req.uri.path_and_query().unwrap());
1223        assert_eq!(new_path.as_bytes(), req.raw_path());
1224        assert!(req.raw_path_is_utf8());
1225    }
1226
1227    #[test]
1228    fn test_invalid_path_with_leading_slash_reaches_the_uri() {
1229        // The same bytes in origin-form take the early return, which stores the lossy
1230        // rendering instead of rooting: from_utf8_lossy always yields valid UTF-8, and the
1231        // high bytes it produces are accepted as a path without relying on the linked http
1232        // crate. Contrast test_invalid_path, where these bytes have no leading slash and so
1233        // no representable component at all.
1234        for (raw_path, expected) in [
1235            (&b"/Hello\xF0\x90\x80World"[..], "/Hello\u{FFFD}World"),
1236            (b"/Hello\xF0\x90\x80World?q=1", "/Hello\u{FFFD}World?q=1"),
1237            (b"/\xF0\x90\x80", "/\u{FFFD}"),
1238        ] {
1239            let req = RequestHeader::build("GET", raw_path, None).unwrap();
1240            let label = String::from_utf8_lossy(raw_path);
1241            assert_eq!(expected, req.uri.path_and_query().unwrap(), "{label}");
1242            assert_eq!(raw_path, req.raw_path(), "{label}");
1243            assert!(!req.raw_path_is_utf8(), "{label}");
1244        }
1245    }
1246
1247    #[test]
1248    fn test_absolute_form_http() {
1249        // The Uri exposes the path component, while raw_path() keeps the target
1250        // verbatim for the wire.
1251        let req = RequestHeader::build("GET", b"http://host/path?query=1", None).unwrap();
1252        assert_eq!("/path?query=1", req.uri.path_and_query().unwrap().as_str());
1253        assert_eq!("/path", req.uri.path());
1254        assert_eq!(b"http://host/path?query=1", req.raw_path());
1255    }
1256
1257    #[test]
1258    fn test_absolute_form_https() {
1259        let req = RequestHeader::build("GET", b"https://example.com/a/b/c?d=e", None).unwrap();
1260        assert_eq!("/a/b/c?d=e", req.uri.path_and_query().unwrap().as_str());
1261        assert_eq!("/a/b/c", req.uri.path());
1262    }
1263
1264    #[test]
1265    fn test_absolute_form_no_path() {
1266        // No path component, so the Uri is left at its default of "/".
1267        let req = RequestHeader::build("GET", b"http://host", None).unwrap();
1268        assert_eq!("/", req.uri.path());
1269        assert_eq!(Some("/"), req.uri.path_and_query().map(|pq| pq.as_str()));
1270        assert_eq!(b"http://host", req.raw_path());
1271    }
1272
1273    #[test]
1274    fn test_absolute_form_root() {
1275        let req = RequestHeader::build("GET", b"http://host/", None).unwrap();
1276        assert_eq!("/", req.uri.path());
1277    }
1278
1279    #[test]
1280    fn test_absolute_form_no_path_with_query() {
1281        // "http://host?query" has no path; origin-form requires at least "/"
1282        // (§3.2.1), so the query is anchored to the root on the stored Uri.
1283        let req = RequestHeader::build("GET", b"http://host?query=1", None).unwrap();
1284        assert_eq!("/", req.uri.path());
1285        assert_eq!(Some("query=1"), req.uri.query());
1286        assert_eq!("/?query=1", req.uri.path_and_query().unwrap().as_str());
1287        assert_eq!(b"http://host?query=1", req.raw_path());
1288    }
1289
1290    #[test]
1291    fn test_absolute_form_uri_has_no_authority() {
1292        // Scheme and authority are deliberately kept off the Uri: the proxy layer
1293        // reconciles the target authority against `Host` from the raw bytes, and a
1294        // populated uri.authority() would divert it to rewriting the target instead.
1295        let req = RequestHeader::build("GET", b"http://host:8080/path?q=1", None).unwrap();
1296        assert_eq!(None, req.uri.scheme_str());
1297        assert_eq!(None, req.uri.authority());
1298        assert_eq!("/path", req.uri.path());
1299        assert_eq!(b"http://host:8080/path?q=1", req.raw_path());
1300    }
1301
1302    #[test]
1303    fn test_fragment_is_not_forwarded() {
1304        // A fragment is not part of the request-target (§3.2) and is separated from the
1305        // URI before dereference (RFC 3986 §3.5), so it must not reach the upstream
1306        // request-line.
1307        for (target, raw, path) in [
1308            (&b"http://host/p#frag"[..], &b"http://host/p"[..], "/p"),
1309            (b"http://host#frag", b"http://host", "/"),
1310            (b"http://host?q=1#frag", b"http://host?q=1", "/"),
1311            // Both authority classifiers terminate at `#`, so a fragment cannot smuggle
1312            // userinfo or a second authority past `Host` reconciliation.
1313            (b"http://host#@evil.example/", b"http://host", "/"),
1314        ] {
1315            let req = RequestHeader::build("GET", target, None).unwrap();
1316            let target = String::from_utf8_lossy(target);
1317            assert_eq!(raw, req.raw_path(), "{target}");
1318            assert_eq!(path, req.uri.path(), "{target}");
1319        }
1320
1321        // Origin-form fragments are already dropped by the Uri, so stripping here keeps
1322        // absolute-form consistent with origin-form rather than diverging from it.
1323        let req = RequestHeader::build("GET", b"/p#frag", None).unwrap();
1324        assert_eq!(b"/p", req.raw_path());
1325
1326        // CONNECT reconciles `Host` against the `#`-truncated prefix, so the stored
1327        // bytes are exactly the ones that were validated.
1328        let req = RequestHeader::build("CONNECT", b"host:443#x", None).unwrap();
1329        assert_eq!(b"host:443", req.raw_path());
1330
1331        // `#` is ASCII, so the strip applies to targets that are not valid UTF-8 too.
1332        // Those are forwarded verbatim, so a fragment left on them would reach the wire.
1333        let req = RequestHeader::build("GET", b"http://host/p\xff#frag", None).unwrap();
1334        assert_eq!(b"http://host/p\xff", req.raw_path());
1335        assert!(!req.raw_path_is_utf8());
1336
1337        // Origin-form takes the early return, so it reaches the strip by a different path
1338        // than the absolute-form case above. Stripping before the UTF-8 check is what keeps
1339        // the two consistent: the Uri drops a fragment on its own, but these bytes go to
1340        // the wire verbatim, and `validate_connect_authority` truncates at `#` regardless.
1341        let req = RequestHeader::build("GET", b"/a\xff#frag", None).unwrap();
1342        assert_eq!(b"/a\xff", req.raw_path());
1343        assert!(!req.raw_path_is_utf8());
1344    }
1345
1346    #[test]
1347    fn test_target_that_is_only_a_fragment_falls_back_to_root() {
1348        // Stripping the fragment can leave nothing behind. There is nothing useful to keep
1349        // verbatim for an empty target, so it resolves through the Uri, which renders it as
1350        // "/" -- the same target these produced before, now stated by the variant rather
1351        // than inferred from a zero-length byte vector.
1352        for target in [&b""[..], b"#", b"#frag", b"#/admin"] {
1353            let req = RequestHeader::build("GET", target, None).unwrap();
1354            let label = String::from_utf8_lossy(target);
1355            assert_eq!(b"/", req.raw_path(), "{label}");
1356            assert_eq!(RawTarget::FromUri, req.raw_target, "{label}");
1357        }
1358
1359        // Asterisk-form and query-only targets survive their fragment rather than
1360        // collapsing to the root, because the Uri round-trips both.
1361        let req = RequestHeader::build("OPTIONS", b"*#frag", None).unwrap();
1362        assert_eq!(b"*", req.raw_path());
1363        assert_eq!(Some("*"), req.uri.path_and_query().map(|pq| pq.as_str()));
1364
1365        let req = RequestHeader::build("GET", b"?q=1#frag", None).unwrap();
1366        assert_eq!(b"?q=1", req.raw_path());
1367        assert_eq!(Some("q=1"), req.uri.query());
1368    }
1369
1370    #[test]
1371    fn test_non_utf8_target_still_yields_a_path() {
1372        // The classifier reads raw bytes, so a non-UTF-8 target must not fall back to
1373        // putting the whole URL into the Uri: uri.path() is what applications route on,
1374        // and it has to agree with the authority that was validated.
1375        let req = RequestHeader::build("GET", b"http://host/p\xff", None).unwrap();
1376        assert_eq!(b"http://host/p\xff", req.raw_path());
1377        assert!(!req.raw_path_is_utf8());
1378        assert_eq!(None, req.uri.authority());
1379        assert_eq!("/p\u{FFFD}", req.uri.path());
1380
1381        // The authority-form CONNECT target is preserved in raw_path even when non-UTF-8.
1382        // Its Uri is rooted like any other target with no absolute-form authority.
1383        let req = RequestHeader::build("CONNECT", b"ho\xffst:443", None).unwrap();
1384        assert_eq!(b"ho\xffst:443", req.raw_path());
1385        assert_eq!("/", req.uri.path());
1386        assert!(!req.raw_path_is_utf8());
1387
1388        // Origin-form keeps its lossy rendering and its original bytes.
1389        let req = RequestHeader::build("GET", b"/p-\xff", None).unwrap();
1390        assert_eq!(b"/p-\xff", req.raw_path());
1391        assert_eq!("/p-\u{FFFD}", req.uri.path());
1392    }
1393
1394    #[test]
1395    fn test_query_only_target_keeps_its_query() {
1396        // "?q=1" carries no authority and no path, but it does carry a query. Dropping it
1397        // would leave filters and cache keys reading a query-less Uri while the upstream
1398        // receives the query.
1399        let req = RequestHeader::build("GET", b"?q=1", None).unwrap();
1400        assert_eq!(b"?q=1", req.raw_path());
1401        assert_eq!(Some("q=1"), req.uri.query());
1402        assert_eq!("/", req.uri.path());
1403        assert_eq!(RawTarget::FromUri, req.raw_target);
1404    }
1405
1406    #[test]
1407    fn test_set_uri_clears_non_origin_form_target() {
1408        // A filter rewriting the target through set_uri must not leave absolute-form or
1409        // CONNECT bytes behind to be serialized.
1410        let mut req = RequestHeader::build("GET", b"http://host/abs?q=1", None).unwrap();
1411        req.set_uri("/replaced".parse().unwrap());
1412        assert_eq!(b"/replaced", req.raw_path());
1413        assert_eq!(RawTarget::FromUri, req.raw_target);
1414
1415        let mut req = RequestHeader::build("CONNECT", b"example.com:443", None).unwrap();
1416        req.set_uri("/replaced".parse().unwrap());
1417        assert_eq!(b"/replaced", req.raw_path());
1418        assert!(req.raw_path_is_utf8());
1419    }
1420
1421    #[test]
1422    fn test_unclassifiable_targets_are_anchored_to_the_root() {
1423        // Targets without absolute-form authority remain in raw_path() for the wire and
1424        // have a rooted Uri, independent of the linked http crate. A second parser
1425        // splitting at the first "://" could extract "/admin", although the reconciled
1426        // classifier stops at the first scheme-terminating colon and never validates that
1427        // path; rooting proves no such path was extracted.
1428        for target in [
1429            &b"foo:bar://evil.example/admin"[..],
1430            b"myproto:x://evil.example/admin",
1431            b"myproto:opaque",
1432        ] {
1433            let req = RequestHeader::build("GET", target, None).unwrap();
1434            let label = String::from_utf8_lossy(target);
1435            assert_eq!(
1436                RawTargetAuthority::None,
1437                raw_target_authority(req.raw_path()),
1438                "{label}"
1439            );
1440            // No absolute-form authority means no path component to isolate, so "/admin"
1441            // cannot appear here.
1442            assert_eq!("/", req.uri.path(), "{label}");
1443            // Wire serialization uses raw_path(), which is verbatim.
1444            assert_eq!(target, req.raw_path(), "{label}");
1445        }
1446
1447        // An ambiguous authority is anchored for the same reason: normalization could move
1448        // the authority boundary, so no path split out of it would be trustworthy.
1449        let req = RequestHeader::build("GET", b"http:///path", None).unwrap();
1450        assert_eq!("/", req.uri.path());
1451        assert_eq!(b"http:///path", req.raw_path());
1452    }
1453
1454    #[test]
1455    fn test_relative_target_without_a_scheme_is_anchored_to_the_root() {
1456        // These targets have neither an authority nor a valid path-and-query, so their
1457        // bytes remain in raw_path() for the H1 wire while the Uri is rooted. H2 egress
1458        // derives :path from the Uri; "/" is required because "foo/bar" is not an absolute
1459        // path and therefore is invalid for :path (RFC 9113 section 8.3.1). See
1460        // test_h2_path_is_rooted_for_targets_with_no_authority for this trade.
1461        for target in [&b"foo/bar"[..], b"host/admin", b"foo", b"foo?q=1"] {
1462            let req = RequestHeader::build("GET", target, None).unwrap();
1463            let label = String::from_utf8_lossy(target);
1464            assert_eq!(
1465                RawTargetAuthority::None,
1466                raw_target_authority(req.raw_path()),
1467                "{label}"
1468            );
1469            assert_eq!("/", req.uri.path_and_query().unwrap(), "{label}");
1470            assert_eq!(target, req.raw_path(), "{label}");
1471        }
1472    }
1473
1474    #[test]
1475    fn test_origin_form_raw_path_is_byte_identical() {
1476        // Origin-form is the hot path and must round-trip through the Uri untouched.
1477        for target in [
1478            &b"/"[..],
1479            b"/index.html",
1480            b"/a/b/c?d=e&f=g",
1481            b"/%2e%2e/x",
1482            b"/a+b/c%20d",
1483            b"*",
1484        ] {
1485            let req = RequestHeader::build("GET", target, None).unwrap();
1486            assert_eq!(
1487                target,
1488                req.raw_path(),
1489                "{}",
1490                String::from_utf8_lossy(target)
1491            );
1492            assert!(req.raw_path_is_utf8());
1493        }
1494    }
1495
1496    #[test]
1497    fn test_non_origin_form_survives_clone_and_parts_round_trip() {
1498        let req = RequestHeader::build("GET", b"http://host:8080/p?q=1", None).unwrap();
1499
1500        let cloned = req.clone();
1501        assert_eq!(req.raw_path(), cloned.raw_path());
1502        assert_eq!(req.uri.path(), cloned.uri.path());
1503        assert_eq!(req.raw_path_is_utf8(), cloned.raw_path_is_utf8());
1504
1505        // ReqParts carries no raw target, so the round-trip falls back to the Uri and
1506        // yields the origin-form path rather than the original absolute-form target.
1507        let from_parts = RequestHeader::from(req.as_owned_parts());
1508        assert_eq!(b"/p?q=1", from_parts.raw_path());
1509        assert!(from_parts.raw_path_is_utf8());
1510    }
1511
1512    #[test]
1513    fn test_connect_authority_form() {
1514        // Authority-form (§3.2.3) is CONNECT's only target form and must reach the tunnel
1515        // destination verbatim through raw_path(). Storing "example.com:443" as
1516        // path-and-query makes construction depend on whether the linked http version
1517        // requires origin-form's leading slash; rejection would break every CONNECT, not
1518        // one field. Rooting the Uri makes construction version-independent.
1519        let req = RequestHeader::build("CONNECT", b"example.com:443", None).unwrap();
1520        assert_eq!(b"example.com:443", req.raw_path());
1521        assert_eq!(None, req.uri.authority());
1522        assert_eq!("/", req.uri.path());
1523    }
1524
1525    #[test]
1526    fn test_connect_authority_form_shapes_pass_through() {
1527        // Parsing deliberately does not validate the authority grammar: that belongs to
1528        // the authority module, which applications running custom protocols can opt out
1529        // of. What this level guarantees is that whichever shape arrives reaches the
1530        // tunnel destination byte-identically, covering both the IP-literal and reg-name
1531        // forms of RFC 3986 §3.2.2. The entire target is preserved in raw_path() for the wire.
1532        for target in [
1533            &b"[v7.x]:443"[..],
1534            b"[vF.a:b~!$&'()*+,;=]:8443",
1535            b"[::1]:443",
1536            b"[2001:db8::1]:8443",
1537            b"127.0.0.1:443",
1538            b"sub.example.com:8080",
1539            b"host-with-dash:1",
1540            b"a_b:443",
1541        ] {
1542            let req = RequestHeader::build("CONNECT", target, None).unwrap();
1543            let label = String::from_utf8_lossy(target);
1544            assert_eq!(target, req.raw_path(), "{label}");
1545            // These invalid path-and-query forms must construct regardless of which http
1546            // version is linked.
1547            assert_eq!("/", req.uri.path(), "{label}");
1548        }
1549    }
1550
1551    #[test]
1552    fn test_set_raw_path_replaces_all_target_state() {
1553        // set_raw_path computes every field before storing any of them, so a mutation
1554        // leaves nothing from the previous target behind.
1555        //
1556        // The rejection half of that contract is not covered here: the pinned http
1557        // fork accepts every request-target, including spaces and control bytes, so
1558        // the error path is unreachable in this configuration. Forbidden bytes are
1559        // caught when the request-line is serialized instead.
1560        let mut req = RequestHeader::build("GET", b"/path-\xff", None).unwrap();
1561        assert!(!req.raw_path_is_utf8());
1562        assert!(matches!(req.raw_target, RawTarget::Lossy(_)));
1563
1564        req.set_raw_path(b"/plain").unwrap();
1565        assert_eq!(b"/plain", req.raw_path());
1566        assert_eq!("/plain", req.uri.path());
1567        assert!(req.raw_path_is_utf8());
1568        assert_eq!(RawTarget::FromUri, req.raw_target);
1569
1570        req.set_raw_path(b"http://host/abs?q=1").unwrap();
1571        assert_eq!(b"http://host/abs?q=1", req.raw_path());
1572        assert_eq!("/abs", req.uri.path());
1573        assert!(req.raw_path_is_utf8());
1574    }
1575
1576    #[test]
1577    fn test_raw_target_variant_per_request_target_form() {
1578        // The variant alone decides both the wire bytes and whether they are UTF-8.
1579        // Storing "is there a stored copy" and "is it UTF-8" as separate fields allowed a
1580        // fourth, meaningless combination, and an empty stored copy that read as an empty
1581        // request-target rather than as "defer to the Uri".
1582        let req = RequestHeader::build("GET", b"http://host/path", None).unwrap();
1583        assert_eq!(
1584            RawTarget::Verbatim(b"http://host/path".to_vec().into()),
1585            req.raw_target
1586        );
1587        assert!(req.raw_path_is_utf8());
1588
1589        let req = RequestHeader::build("CONNECT", b"example.com:443", None).unwrap();
1590        assert_eq!(
1591            RawTarget::Verbatim(b"example.com:443".to_vec().into()),
1592            req.raw_target
1593        );
1594        assert!(req.raw_path_is_utf8());
1595
1596        let req = RequestHeader::build("GET", b"/path-\xff", None).unwrap();
1597        assert_eq!(
1598            RawTarget::Lossy(b"/path-\xff".to_vec().into()),
1599            req.raw_target
1600        );
1601        assert!(!req.raw_path_is_utf8());
1602
1603        for target in [&b"/path"[..], b"*"] {
1604            let req = RequestHeader::build("GET", target, None).unwrap();
1605            let label = String::from_utf8_lossy(target);
1606            assert_eq!(RawTarget::FromUri, req.raw_target, "{label}");
1607            assert!(req.raw_path_is_utf8(), "{label}");
1608        }
1609    }
1610
1611    #[test]
1612    fn test_set_raw_path_clears_stale_connect_fallback() {
1613        // Reusing a header: the CONNECT authority-form target must not survive into
1614        // a subsequent origin-form request via a stale RawTarget::Verbatim.
1615        let mut req = RequestHeader::build("CONNECT", b"example.com:443", None).unwrap();
1616        assert_eq!(b"example.com:443", req.raw_path());
1617        req.set_method(Method::GET);
1618        req.set_raw_path(b"/ok").unwrap();
1619        assert_eq!(b"/ok", req.raw_path());
1620        assert_eq!("/ok", req.uri.path());
1621    }
1622
1623    #[test]
1624    fn test_absolute_form_set_raw_path_mutation() {
1625        // The mutation path, not just construction via build().
1626        let mut req = RequestHeader::build("GET", b"/original", None).unwrap();
1627        assert_eq!("/original", req.uri.path());
1628        req.set_raw_path(b"http://host/mutated?q=1").unwrap();
1629        assert_eq!("/mutated?q=1", req.uri.path_and_query().unwrap().as_str());
1630        assert_eq!("/mutated", req.uri.path());
1631    }
1632
1633    #[test]
1634    fn test_absolute_form_with_port() {
1635        let req = RequestHeader::build("GET", b"http://host:8080/path", None).unwrap();
1636        assert_eq!("/path", req.uri.path());
1637    }
1638
1639    #[test]
1640    fn test_absolute_form_uppercase_scheme() {
1641        // RFC 3986 §3.1: scheme is case-insensitive.
1642        let req = RequestHeader::build("GET", b"HTTP://HOST/path", None).unwrap();
1643        assert_eq!("/path", req.uri.path());
1644    }
1645
1646    #[test]
1647    fn test_absolute_form_non_http_scheme() {
1648        // scheme().is_some() admits any valid scheme, not just http/https.
1649        let req = RequestHeader::build("GET", b"ftp://host/path", None).unwrap();
1650        assert_eq!("/path", req.uri.path());
1651    }
1652
1653    #[test]
1654    fn test_origin_form_unchanged() {
1655        let req = RequestHeader::build("GET", b"/path?q=1", None).unwrap();
1656        assert_eq!("/path?q=1", req.uri.path_and_query().unwrap().as_str());
1657    }
1658
1659    #[test]
1660    fn test_origin_form_with_scheme_in_query() {
1661        // An origin-form path whose query contains "://" must not be mistaken
1662        // for absolute-form (guarded by the starts_with('/') fast path).
1663        let req = RequestHeader::build("GET", b"/redir?url=http://other", None).unwrap();
1664        assert_eq!(
1665            "/redir?url=http://other",
1666            req.uri.path_and_query().unwrap().as_str()
1667        );
1668    }
1669
1670    #[test]
1671    fn test_asterisk_form_unchanged() {
1672        let req = RequestHeader::build("OPTIONS", b"*", None).unwrap();
1673        assert_eq!("*", req.uri.path_and_query().unwrap().as_str());
1674    }
1675
1676    #[test]
1677    fn test_authority_form_raw_path() {
1678        let mut req = RequestHeader::new_no_case(None);
1679        req.set_method(Method::CONNECT);
1680        req.set_uri(Uri::builder().authority("pingora.org:443").build().unwrap());
1681
1682        assert!(req.uri.path_and_query().is_none());
1683        assert_eq!(b"pingora.org:443", req.raw_path());
1684        assert!(req.raw_path_is_utf8());
1685    }
1686
1687    #[test]
1688    fn test_reason_phrase() {
1689        let mut resp = ResponseHeader::new(None);
1690        let reason = resp.get_reason_phrase().unwrap();
1691        assert_eq!(reason, "OK");
1692
1693        resp.set_reason_phrase(Some("FooBar")).unwrap();
1694        let reason = resp.get_reason_phrase().unwrap();
1695        assert_eq!(reason, "FooBar");
1696
1697        resp.set_reason_phrase(Some("OK")).unwrap();
1698        let reason = resp.get_reason_phrase().unwrap();
1699        assert_eq!(reason, "OK");
1700
1701        resp.set_reason_phrase(None).unwrap();
1702        let reason = resp.get_reason_phrase().unwrap();
1703        assert_eq!(reason, "OK");
1704    }
1705
1706    #[test]
1707    fn set_test_send_end_stream() {
1708        let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1709        req.set_send_end_stream(true);
1710
1711        // None for requests that are not h2
1712        assert!(req.send_end_stream().is_none());
1713
1714        let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1715        req.set_version(Version::HTTP_2);
1716
1717        // Some(true) by default for h2
1718        assert!(req.send_end_stream().unwrap());
1719
1720        req.set_send_end_stream(false);
1721        // Some(false)
1722        assert!(!req.send_end_stream().unwrap());
1723    }
1724
1725    #[test]
1726    fn set_test_set_content_length() {
1727        let mut resp = ResponseHeader::new(None);
1728        resp.set_content_length(10).unwrap();
1729
1730        assert_eq!(
1731            b"10",
1732            resp.headers
1733                .get(http::header::CONTENT_LENGTH)
1734                .map(|d| d.as_bytes())
1735                .unwrap()
1736        );
1737    }
1738
1739    #[test]
1740    fn normalize_field_value_no_fold_is_zero_copy() {
1741        // The value has no newline at all -> the input `Bytes` must be
1742        // returned untouched (same allocation). We assert pointer/length
1743        // identity via `Bytes::ptr_eq` semantics: cloning a `Bytes` shares
1744        // the same underlying buffer, so comparing byte content + length
1745        // is sufficient to confirm no allocation happened.
1746        let input = bytes::Bytes::from_static(b"text/html; charset=utf-8");
1747        let out = normalize_field_value(input.clone());
1748        assert_eq!(out, input);
1749        assert_eq!(out.as_ptr(), input.as_ptr());
1750    }
1751
1752    #[test]
1753    fn normalize_field_value_single_fold() {
1754        // CRLF + SP continuation collapses to a single SP.
1755        let input = bytes::Bytes::from_static(b"obs\r\n fold");
1756        assert_eq!(&normalize_field_value(input)[..], b"obs fold");
1757    }
1758
1759    #[test]
1760    fn normalize_field_value_multiple_folds_mixed_ws() {
1761        // "obs\r\n fold\r\n\t line" -> "obs fold line". Each fold becomes
1762        // exactly one SP regardless of how many SP/HTAB chars the
1763        // continuation indented with.
1764        let input = bytes::Bytes::from_static(b"obs\r\n fold\r\n\t line");
1765        assert_eq!(&normalize_field_value(input)[..], b"obs fold line");
1766    }
1767
1768    #[test]
1769    fn normalize_field_value_collapses_long_indent() {
1770        // Real-world CSP-style values often indent continuations with many
1771        // spaces. All of that indent collapses to a single SP.
1772        let input =
1773            bytes::Bytes::from_static(b"default-src 'self';\r\n        script-src 'self' blob:");
1774        assert_eq!(
1775            &normalize_field_value(input)[..],
1776            b"default-src 'self'; script-src 'self' blob:"
1777        );
1778    }
1779
1780    #[test]
1781    fn normalize_field_value_removes_all_cr_and_lf() {
1782        // After normalization no CR or LF byte may survive in the value
1783        // (each obs-fold collapses to a single SP).
1784        let input = bytes::Bytes::from_static(b"a\r\n b\r\n c\r\n d");
1785        let out = normalize_field_value(input);
1786        assert!(!out.contains(&b'\r'));
1787        assert!(!out.contains(&b'\n'));
1788        assert_eq!(&out[..], b"a b c d");
1789    }
1790
1791    #[test]
1792    fn normalize_field_value_bare_lf() {
1793        // Defensive: bare LF (no preceding CR) is also treated as a fold,
1794        // since the implementation splits on `\n` alone.
1795        let input = bytes::Bytes::from_static(b"obs\n fold");
1796        assert_eq!(&normalize_field_value(input)[..], b"obs fold");
1797    }
1798
1799    #[test]
1800    fn normalize_field_value_empty() {
1801        let input = bytes::Bytes::new();
1802        let out = normalize_field_value(input.clone());
1803        assert_eq!(out, input);
1804    }
1805
1806    #[test]
1807    fn header_value_from_raw_round_trip() {
1808        // End-to-end: a folded value parses into a HeaderValue whose bytes
1809        // contain no CR/LF and equal the normalized form.
1810        let hv = header_value_from_raw(bytes::Bytes::from_static(
1811            b"default-src 'self';\r\n script-src 'self'",
1812        ));
1813        assert_eq!(hv.as_bytes(), b"default-src 'self'; script-src 'self'");
1814    }
1815
1816    #[test]
1817    fn header_value_from_raw_passthrough() {
1818        // No fold -> bytes preserved exactly.
1819        let hv = header_value_from_raw(bytes::Bytes::from_static(b"application/json"));
1820        assert_eq!(hv.as_bytes(), b"application/json");
1821    }
1822
1823    // The remaining tests pin down the contract on edge-case inputs.
1824    // `header_value_from_raw` is `pub`, so any caller (not just our own
1825    // httparse-driven paths) can pass arbitrary bytes; these cases
1826    // document what they will get back.
1827
1828    #[test]
1829    fn normalize_field_value_leading_newline_no_spurious_space() {
1830        // A value starting with a fold collapses to just the continuation,
1831        // with no spurious leading SP.
1832        let input = bytes::Bytes::from_static(b"\r\n fold");
1833        assert_eq!(&normalize_field_value(input)[..], b"fold");
1834    }
1835
1836    #[test]
1837    fn normalize_field_value_trailing_newline_no_spurious_space() {
1838        // A trailing CRLF (or CRLF + WSP that ends the value) is dropped
1839        // without leaving a trailing SP.
1840        let input = bytes::Bytes::from_static(b"foo\r\n");
1841        assert_eq!(&normalize_field_value(input)[..], b"foo");
1842
1843        let input = bytes::Bytes::from_static(b"a\r\n b\r\n");
1844        assert_eq!(&normalize_field_value(input)[..], b"a b");
1845    }
1846
1847    #[test]
1848    fn normalize_field_value_only_newlines() {
1849        // Pathological input made entirely of newlines collapses to empty.
1850        assert_eq!(
1851            &normalize_field_value(bytes::Bytes::from_static(b"\r\n"))[..],
1852            b""
1853        );
1854        assert_eq!(
1855            &normalize_field_value(bytes::Bytes::from_static(b"\n"))[..],
1856            b""
1857        );
1858        assert_eq!(
1859            &normalize_field_value(bytes::Bytes::from_static(b"\r\n\r\n"))[..],
1860            b""
1861        );
1862    }
1863
1864    #[test]
1865    fn normalize_field_value_replaces_bare_cr_with_sp() {
1866        // Per RFC 9110 section 5.5, stray CR / LF / NUL within a field
1867        // value MUST be replaced with SP (not stripped, not left in place).
1868        let cases: &[(&[u8], &[u8])] = &[
1869            (b"foo\rbar", b"foo bar"),
1870            (b"foo\r", b"foo "),
1871            (b"\rfoo", b" foo"),
1872            (b"\r", b" "),
1873            (b"\r\r\r", b"   "),
1874        ];
1875        for (input, expected) in cases {
1876            let out = normalize_field_value(bytes::Bytes::copy_from_slice(input));
1877            assert_eq!(&out[..], *expected, "input = {input:?}");
1878            assert!(!out.contains(&b'\r'));
1879            assert!(!out.contains(&b'\n'));
1880            assert!(!out.contains(&b'\0'));
1881        }
1882    }
1883
1884    #[test]
1885    fn normalize_field_value_replaces_cr_mid_segment_with_sp() {
1886        // Mid-segment CR (sits inside a segment, not at a boundary the
1887        // trim helpers reach) is replaced with SP per RFC 9110 section 5.5.
1888        let input = bytes::Bytes::from_static(b"foo\rbar\r\n baz");
1889        assert_eq!(&normalize_field_value(input)[..], b"foo bar baz");
1890    }
1891
1892    #[test]
1893    fn normalize_field_value_replaces_nul_with_sp() {
1894        // NUL within a field value MUST be replaced with SP per RFC 9110
1895        // section 5.5. Covers NUL standalone, between CR and LF, and after
1896        // CRLF.
1897        let cases: &[(&[u8], &[u8])] = &[
1898            (b"foo\0bar", b"foo bar"),
1899            (b"\0\0\0", b"   "),
1900            (b"foo\0", b"foo "),
1901            // NUL between CR and LF: each is a stray byte -> three SPs.
1902            (b"foo\r\0\nbar", b"foo   bar"),
1903            // NUL after CRLF: CRLF treated as a fold boundary (one SP),
1904            // NUL replaced with one SP -> two SPs total.
1905            (b"foo\r\n\0bar", b"foo  bar"),
1906        ];
1907        for (input, expected) in cases {
1908            let out = normalize_field_value(bytes::Bytes::copy_from_slice(input));
1909            assert_eq!(&out[..], *expected, "input = {input:?}");
1910            assert!(!out.contains(&b'\r'));
1911            assert!(!out.contains(&b'\n'));
1912            assert!(!out.contains(&b'\0'));
1913        }
1914    }
1915
1916    #[test]
1917    fn header_value_from_raw_handles_invalid_bytes() {
1918        // Bare CR and NUL aren't valid `field-content`, but
1919        // `normalize_field_value` replaces them with SP before the
1920        // unchecked constructor sees them, so no invalid byte ever reaches
1921        // `HeaderValue`.
1922        let hv = header_value_from_raw(bytes::Bytes::from_static(b"foo\rbar\0baz"));
1923        assert_eq!(hv.as_bytes(), b"foo bar baz");
1924    }
1925}