Skip to main content

rama_http/layer/version_adapter/
request.rs

1use rama_core::Layer;
2use rama_core::Service;
3use rama_core::bytes::BytesMut;
4use rama_core::error::BoxError;
5use rama_core::error::ErrorContext;
6use rama_core::extensions::ExtensionsRef;
7use rama_core::telemetry::tracing;
8use rama_http_headers::Connection;
9use rama_http_headers::HeaderMapExt;
10use rama_http_headers::Host;
11use rama_http_headers::SecWebSocketKey;
12use rama_http_headers::SecWebSocketVersion;
13use rama_http_headers::Upgrade;
14use rama_http_types::HeaderValue;
15use rama_http_types::Method;
16use rama_http_types::Request;
17use rama_http_types::Version;
18use rama_http_types::conn::TargetHttpVersion;
19use rama_http_types::header::COOKIE;
20use rama_http_types::header::Entry;
21use rama_http_types::header::HOST;
22use rama_http_types::header::{SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_VERSION};
23use rama_http_types::proto::h2::ext::Protocol;
24use rama_net::client::{ConnectorService, EstablishedClientConnection};
25use rama_net::{AuthorityInputExt, Protocol as Scheme, ProtocolInputExt};
26
27use crate::layer::remove_header::remove_illegal_h2_request_headers;
28use rama_utils::macros::generate_set_and_with;
29
30#[derive(Clone, Debug)]
31/// [`ConnectorService`] which will adapt the request version if needed.
32///
33/// It will adapt the request version to [`TargetHttpVersion`], or the configured
34/// default version
35pub struct RequestVersionAdapter<S> {
36    inner: S,
37    default_http_version: Option<Version>,
38}
39
40impl<S> RequestVersionAdapter<S> {
41    pub fn new(inner: S) -> Self {
42        Self {
43            inner,
44            default_http_version: None,
45        }
46    }
47
48    generate_set_and_with! {
49        /// Set default request [`Version`] which will be used if [`TargetHttpVersion`] is
50        /// is not present in extensions
51        pub fn default_version(mut self, version: Option<Version>) -> Self {
52            self.default_http_version = version;
53            self
54        }
55    }
56}
57
58impl<S, Body> Service<Request<Body>> for RequestVersionAdapter<S>
59where
60    S: ConnectorService<Request<Body>, Error: Into<BoxError>>,
61    Body: Send + 'static,
62{
63    type Output = EstablishedClientConnection<S::Connection, Request<Body>>;
64    type Error = BoxError;
65
66    async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
67        let EstablishedClientConnection {
68            conn,
69            input: mut req,
70        } = self.inner.connect(req).await.into_box_error()?;
71
72        let version = req
73            .extensions()
74            .clone_to_if_absent::<TargetHttpVersion>(conn.extensions())
75            .map(|version| version.0);
76
77        match (version, self.default_http_version) {
78            (Some(version), _) => {
79                tracing::trace!(
80                    "setting request version to {:?} based on configured TargetHttpVersion (was: {:?})",
81                    version,
82                    req.version(),
83                );
84                adapt_request_version(&mut req, version)?;
85            }
86            (_, Some(version)) => {
87                tracing::trace!(
88                    "setting request version to {:?} based on configured default http version (was: {:?})",
89                    version,
90                    req.version(),
91                );
92                adapt_request_version(&mut req, version)?;
93
94                // Since this default is now the actual target, also store this on the connection so other components
95                // can see this. This is needed in case this adapter is used twice e.g. with connection pooling
96                conn.extensions().insert(TargetHttpVersion(version));
97            }
98            (None, None) => {
99                tracing::trace!(
100                    "no TargetHttpVersion or default http version configured, leaving request version {:?}",
101                    req.version(),
102                );
103            }
104        }
105
106        Ok(EstablishedClientConnection { input: req, conn })
107    }
108}
109
110#[derive(Clone, Debug, Default)]
111/// [`ConnectorService`] layer which will adapt the request version if needed.
112///
113/// It will adapt the request version to [`TargetHttpVersion`], or the configured
114/// default version
115pub struct RequestVersionAdapterLayer {
116    default_http_version: Option<Version>,
117}
118
119impl RequestVersionAdapterLayer {
120    #[must_use]
121    pub fn new() -> Self {
122        Self {
123            default_http_version: None,
124        }
125    }
126
127    generate_set_and_with! {
128        /// Set default request [`Version`] which will be used if [`TargetHttpVersion`] is
129        /// is not present in extensions
130        pub fn default_version(mut self, version: Option<Version>) -> Self {
131            self.default_http_version = version;
132            self
133        }
134    }
135}
136
137impl<S> Layer<S> for RequestVersionAdapterLayer {
138    type Service = RequestVersionAdapter<S>;
139
140    fn layer(&self, inner: S) -> Self::Service {
141        RequestVersionAdapter {
142            inner,
143            default_http_version: self.default_http_version,
144        }
145    }
146}
147
148/// Adapt request to match the provided [`Version`]
149pub fn adapt_request_version<Body>(
150    request: &mut Request<Body>,
151    target_version: Version,
152) -> Result<(), BoxError> {
153    let request_version = request.version();
154    if request_version == target_version {
155        tracing::trace!(
156            ?target_version,
157            "request version already satisfied, skipping it"
158        );
159        return Ok(());
160    }
161    tracing::trace!(
162        ?request_version,
163        ?target_version,
164        "changing request version"
165    );
166
167    let request_is_h1 = request_version <= Version::HTTP_11;
168    let target_is_h1 = target_version <= Version::HTTP_11;
169
170    // Translate the handshake (WebSocket method / `:protocol` form) when
171    // crossing the HTTP/1 <-> HTTP/2/3 boundary. Within a class it is already correct.
172    match (request_is_h1, target_is_h1) {
173        (true, false) => translate_request_upgrade(request)?,
174        (false, true) => translate_request_downgrade(request)?,
175        (true, true) | (false, false) => {}
176    }
177
178    // Normalize the request so it is valid for the target version (runs regardless of
179    // whether the version actually changed).
180    *request.version_mut() = target_version;
181    ensure_valid_request_for_version(request)?;
182
183    Ok(())
184}
185
186/// Normalize `request` so it is valid for it's configured `version`
187pub fn ensure_valid_request_for_version<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
188    if request.version() <= Version::HTTP_11 {
189        ensure_valid_h1_request(request)
190    } else {
191        ensure_valid_h2_or_h3_request(request)
192    }
193}
194
195/// Normalize a request so it is a valid HTTP/1.x request: ensure a `Host` header and
196/// collapse multiple `Cookie` headers into one (RFC 6265 §5.4).
197pub fn ensure_valid_h1_request<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
198    ensure_h1_host_header(request)?;
199    merge_cookie_headers_for_http1(request)?;
200    Ok(())
201}
202
203/// Normalize a request so it is a valid HTTP/2 or HTTP/3 request: ensure the
204/// authority/scheme live in the URI (for the `:authority`/`:scheme` pseudo-headers) and
205/// strip the connection-specific headers those versions forbid (RFC 9113 §8.2.2).
206pub fn ensure_valid_h2_or_h3_request<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
207    ensure_h2_or_h3_uri_authority(request)?;
208    remove_illegal_h2_request_headers(request.headers_mut());
209    Ok(())
210}
211
212/// Whether a [`Protocol`] is the WebSocket Extended CONNECT / `Upgrade` protocol.
213pub(crate) fn is_websocket_protocol(protocol: &Protocol) -> bool {
214    protocol.as_str().eq_ignore_ascii_case("websocket")
215}
216
217/// The Extended CONNECT / `Upgrade` application protocol a request is *genuinely*
218/// switching to (e.g. `websocket`), if any.
219///
220/// HTTP/2 and HTTP/3 carry it in the `:protocol` pseudo-header (the [`Protocol`]
221/// extension on a `CONNECT`). HTTP/1 carries it in the `Upgrade` header, but only
222/// counts as a genuine switch when accompanied by `Connection: Upgrade` — otherwise
223/// it is a mere protocol advertisement, which is ignored (not an error).
224pub(crate) fn request_connect_protocol<Body>(request: &Request<Body>) -> Option<Protocol> {
225    if request.method() == Method::CONNECT
226        && let Some(protocol) = request.extensions().get_ref::<Protocol>()
227    {
228        return Some(protocol.clone());
229    }
230
231    let is_genuine_upgrade = request
232        .headers()
233        .typed_get::<Connection>()
234        .is_some_and(|connection| connection.contains_upgrade());
235    if !is_genuine_upgrade {
236        return None;
237    }
238    let upgrade = request.headers().typed_get::<Upgrade>()?;
239    let token = std::str::from_utf8(upgrade.as_bytes()).ok()?.trim();
240    (!token.is_empty()).then(|| Protocol::from(token))
241}
242
243/// Translate the handshake envelope of an HTTP/1.x request up to HTTP/2 or HTTP/3.
244///
245/// Converts a WebSocket `Upgrade` handshake into an Extended CONNECT (RFC 8441 for
246/// HTTP/2, RFC 9220 for HTTP/3 — same `:protocol` pseudo-header). WebSocket is the only
247/// Extended CONNECT protocol whose cross-version translation is supported; any other
248/// genuine upgrade is rejected rather than silently dropped. Header validity (authority,
249/// stripping illegal headers) is handled separately by [`ensure_valid_h2_or_h3_request`].
250fn translate_request_upgrade<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
251    match request_connect_protocol(request) {
252        Some(protocol) if is_websocket_protocol(&protocol) => {
253            // `GET` + `Upgrade: websocket` -> `CONNECT` + `:protocol: websocket`.
254            tracing::trace!("translating h1 websocket upgrade into h2/h3 extended CONNECT");
255            *request.method_mut() = Method::CONNECT;
256            request
257                .extensions()
258                .insert(Protocol::from_static("websocket"));
259        }
260        Some(protocol) => {
261            return Err(BoxError::from(format!(
262                "cannot translate HTTP/1 `Upgrade: {}` into an HTTP/2+ Extended CONNECT: only websocket is supported",
263                protocol.as_str(),
264            )));
265        }
266        None => {}
267    }
268    Ok(())
269}
270
271/// Translate the handshake envelope of an HTTP/2 or HTTP/3 request down to HTTP/1.x.
272///
273/// Converts an Extended CONNECT WebSocket request back into an HTTP/1.x `Upgrade`
274/// handshake. A non-WebSocket Extended CONNECT is rejected, a plain `CONNECT` tunnel
275/// (no `:protocol`) is left untouched. The `Host` header is added separately by
276/// [`ensure_valid_h1_request`].
277fn translate_request_downgrade<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
278    match request_connect_protocol(request) {
279        Some(protocol) if is_websocket_protocol(&protocol) => {
280            // `CONNECT` + `:protocol: websocket` -> `GET` + `Upgrade: websocket`.
281            tracing::trace!("translating h2/h3 extended CONNECT websocket into h1 upgrade");
282            *request.method_mut() = Method::GET;
283
284            let headers = request.headers_mut();
285            headers.typed_insert(Upgrade::websocket());
286            headers.typed_insert(Connection::upgrade());
287            if !headers.contains_key(SEC_WEBSOCKET_KEY) {
288                headers.typed_insert(SecWebSocketKey::random());
289            }
290            if !headers.contains_key(SEC_WEBSOCKET_VERSION) {
291                headers.typed_insert(SecWebSocketVersion::V13);
292            }
293
294            // NOTE: the `:protocol` pseudo-header is carried via the `Protocol`
295            // extension, which is only emitted on HTTP/2 CONNECT requests. It is inert
296            // for HTTP/1 and `Extensions` has no removal API, so we leave it in place.
297        }
298        Some(protocol) => {
299            return Err(BoxError::from(format!(
300                "cannot translate an HTTP/2+ Extended CONNECT `:protocol: {}` request to HTTP/1: only websocket is supported",
301                protocol.as_str(),
302            )));
303        }
304        None => {}
305    }
306    Ok(())
307}
308
309/// Ensure an HTTP/1.x request carries a `Host` header.
310///
311/// HTTP/1 carries the routing authority in the `Host` header, whereas HTTP/2 and
312/// HTTP/3 carry it in the `:authority` pseudo-header (the URI). The `Host` header is
313/// derived from the request authority when missing. Used both when downgrading a
314/// request to HTTP/1 and as a general send-time backstop.
315pub fn ensure_h1_host_header<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
316    if request.headers().contains_key(HOST) {
317        return Ok(());
318    }
319    let authority = request
320        .authority()
321        .context("ensure h1 Host header: request has no resolvable authority")?;
322    let protocol = request.protocol().cloned();
323    // Strip the default port (browsers do this, and some reverse proxies 404 on a
324    // non-exact authority match).
325    let authority = authority.without_default_port_for(protocol.as_ref());
326    tracing::trace!("adding Host header {authority} derived from request authority");
327    request.headers_mut().typed_insert(Host::from(authority));
328    Ok(())
329}
330
331/// Ensure an HTTP/2 or HTTP/3 request carries its authority and scheme in the URI.
332///
333/// HTTP/2 and HTTP/3 require the `:authority`/`:scheme` pseudo-headers, which the
334/// underlying crates derive from the URI. When converting up from HTTP/1 (where the
335/// authority lives in the `Host` header) the URI authority and scheme are materialized
336/// from the request authority if the URI lacks a host. Used both when upgrading a
337/// request to HTTP/2+ and as a general send-time backstop.
338pub fn ensure_h2_or_h3_uri_authority<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
339    if request.uri().host().is_some() {
340        return Ok(());
341    }
342    let authority = request
343        .authority()
344        .context("ensure h2 URI authority: request has no resolvable authority")?;
345    let protocol = request.protocol().cloned();
346    let authority = authority.without_default_port_for(protocol.as_ref());
347    tracing::trace!("materializing authority {authority} and scheme into request URI");
348    let uri = request.uri_mut();
349    uri.set_scheme(protocol.unwrap_or(Scheme::HTTP));
350    uri.set_host(authority.host);
351    uri.set_port(authority.port);
352    Ok(())
353}
354
355/// Merge multiple cookie headers into a single Cookie header for HTTP/1.x compliance
356/// per RFC 6265 §5.4: "the user agent MUST NOT attach more than one Cookie header field"
357fn merge_cookie_headers_for_http1<Body>(request: &mut Request<Body>) -> Result<(), BoxError> {
358    if let Entry::Occupied(cookie_headers) = request.headers_mut().entry(COOKIE) {
359        let Some((bytes_count, header_count)) = cookie_headers
360            .iter()
361            .map(|v| (v.as_bytes().len(), 1usize))
362            .reduce(|a, b| (a.0 + b.0, a.1 + b.1))
363        else {
364            return Ok(());
365        };
366        if header_count <= 1 {
367            return Ok(());
368        }
369
370        let (header_name, mut header_values) = cookie_headers.remove_entry_mult();
371
372        let mut buffer = BytesMut::with_capacity(bytes_count + ((header_count - 1) * 2));
373        if let Some(header_value) = header_values.next() {
374            buffer.extend_from_slice(header_value.as_bytes());
375        }
376        for header_value in header_values {
377            buffer.extend_from_slice(b"; ");
378            buffer.extend_from_slice(header_value.as_bytes());
379        }
380
381        let new_header_value = HeaderValue::from_maybe_shared(buffer)
382            .context("create new cookie header value from combined multiple values")?;
383
384        request.headers_mut().insert(header_name, new_header_value);
385    }
386
387    Ok(())
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use rama_http_types::header::{CONNECTION, COOKIE, HOST, TRANSFER_ENCODING, UPGRADE};
394
395    #[test]
396    fn test_h1_to_h2_strips_connection_specific_headers() {
397        let mut req = Request::builder()
398            .version(Version::HTTP_11)
399            .uri("https://example.com")
400            .header(HOST, "example.com")
401            .header(CONNECTION, "keep-alive, x-custom")
402            .header("keep-alive", "timeout=5")
403            .header(TRANSFER_ENCODING, "chunked")
404            .header("x-custom", "1")
405            .header("x-keep", "yes")
406            .body(())
407            .unwrap();
408
409        adapt_request_version(&mut req, Version::HTTP_2).unwrap();
410
411        assert_eq!(req.version(), Version::HTTP_2);
412        for illegal in [&HOST, &CONNECTION, &TRANSFER_ENCODING] {
413            assert!(
414                !req.headers().contains_key(illegal),
415                "expected {illegal:?} to be removed"
416            );
417        }
418        // header named by the Connection header must also be gone
419        assert!(!req.headers().contains_key("x-custom"));
420        assert!(!req.headers().contains_key("keep-alive"));
421        // unrelated headers are preserved
422        assert_eq!(req.headers().get("x-keep").unwrap(), "yes");
423    }
424
425    #[test]
426    fn test_h1_to_h2_websocket_upgrade_becomes_extended_connect() {
427        let mut req = Request::builder()
428            .version(Version::HTTP_11)
429            .method(Method::GET)
430            .uri("https://example.com/chat")
431            .header(UPGRADE, "websocket")
432            .header(CONNECTION, "Upgrade")
433            .header(SEC_WEBSOCKET_KEY, "dGhlIHNhbXBsZSBub25jZQ==")
434            .header(SEC_WEBSOCKET_VERSION, "13")
435            .header("sec-websocket-protocol", "chat")
436            .body(())
437            .unwrap();
438
439        adapt_request_version(&mut req, Version::HTTP_2).unwrap();
440
441        assert_eq!(req.method(), Method::CONNECT);
442        assert_eq!(
443            req.extensions().get_ref::<Protocol>().map(|p| p.as_str()),
444            Some("websocket"),
445        );
446        // h1 handshake headers are removed for h2 (RFC 8441 §5.1)
447        assert!(!req.headers().contains_key(UPGRADE));
448        assert!(!req.headers().contains_key(CONNECTION));
449        assert!(!req.headers().contains_key(SEC_WEBSOCKET_KEY));
450        // Sec-WebSocket-Version is kept in h2, as is the requested subprotocol
451        assert_eq!(req.headers().get(SEC_WEBSOCKET_VERSION).unwrap(), "13");
452        assert_eq!(req.headers().get("sec-websocket-protocol").unwrap(), "chat");
453    }
454
455    #[test]
456    fn test_h2_to_h1_websocket_connect_becomes_upgrade() {
457        let mut req = Request::builder()
458            .version(Version::HTTP_2)
459            .method(Method::CONNECT)
460            .uri("https://example.com/chat")
461            .header(SEC_WEBSOCKET_VERSION, "13")
462            .body(())
463            .unwrap();
464        req.extensions().insert(Protocol::from_static("websocket"));
465
466        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
467
468        assert_eq!(req.method(), Method::GET);
469        assert_eq!(req.version(), Version::HTTP_11);
470        assert!(
471            req.headers()
472                .typed_get::<Upgrade>()
473                .is_some_and(|u| u.is_websocket())
474        );
475        assert!(
476            req.headers()
477                .typed_get::<Connection>()
478                .is_some_and(|c| c.contains_upgrade())
479        );
480        // a fresh key is generated and the version is retained
481        assert!(req.headers().contains_key(SEC_WEBSOCKET_KEY));
482        assert_eq!(req.headers().get(SEC_WEBSOCKET_VERSION).unwrap(), "13");
483    }
484
485    #[test]
486    fn test_h2_to_h1_non_websocket_connect_untouched() {
487        let mut req = Request::builder()
488            .version(Version::HTTP_2)
489            .method(Method::CONNECT)
490            .uri("example.com:443")
491            .header(HOST, "example.com:443")
492            .body(())
493            .unwrap();
494
495        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
496
497        // plain CONNECT (no :protocol) must not gain websocket headers
498        assert_eq!(req.method(), Method::CONNECT);
499        assert!(!req.headers().contains_key(UPGRADE));
500        assert!(!req.headers().contains_key(SEC_WEBSOCKET_KEY));
501    }
502
503    #[test]
504    fn test_h2_to_h1_adds_host_from_authority() {
505        let mut req = Request::builder()
506            .version(Version::HTTP_2)
507            .uri("https://example.com/path")
508            .body(())
509            .unwrap();
510
511        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
512
513        // HTTP/1 carries the authority in the Host header, derived from the URI
514        assert_eq!(req.version(), Version::HTTP_11);
515        assert_eq!(req.headers().get(HOST).unwrap(), "example.com");
516    }
517
518    #[test]
519    fn test_h1_to_h2_materializes_uri_authority_and_strips_host() {
520        let mut req = Request::builder()
521            .version(Version::HTTP_11)
522            .uri("/path")
523            .header(HOST, "example.com")
524            .body(())
525            .unwrap();
526
527        adapt_request_version(&mut req, Version::HTTP_2).unwrap();
528
529        assert_eq!(req.version(), Version::HTTP_2);
530        // authority now lives in the URI (for :authority/:scheme); Host header is gone
531        assert_eq!(req.uri().host_str().as_deref(), Some("example.com"));
532        assert!(!req.headers().contains_key(HOST));
533    }
534
535    #[test]
536    fn test_h1_to_h3_websocket_upgrade_becomes_extended_connect() {
537        let mut req = Request::builder()
538            .version(Version::HTTP_11)
539            .method(Method::GET)
540            .uri("https://example.com/chat")
541            .header(UPGRADE, "websocket")
542            .header(CONNECTION, "Upgrade")
543            .header(SEC_WEBSOCKET_KEY, "dGhlIHNhbXBsZSBub25jZQ==")
544            .body(())
545            .unwrap();
546
547        adapt_request_version(&mut req, Version::HTTP_3).unwrap();
548
549        // HTTP/3 reuses HTTP/2's Extended CONNECT model (RFC 9220), so the conversion
550        // is identical — proving the class-based translation handles h3 for free.
551        assert_eq!(req.version(), Version::HTTP_3);
552        assert_eq!(req.method(), Method::CONNECT);
553        assert_eq!(
554            req.extensions().get_ref::<Protocol>().map(|p| p.as_str()),
555            Some("websocket"),
556        );
557        assert!(!req.headers().contains_key(UPGRADE));
558        assert!(!req.headers().contains_key(SEC_WEBSOCKET_KEY));
559    }
560
561    #[test]
562    fn test_h2_to_h3_only_changes_version() {
563        let mut req = Request::builder()
564            .version(Version::HTTP_2)
565            .uri("https://example.com/path")
566            .header(COOKIE, "a=1")
567            .header(COOKIE, "b=2")
568            .body(())
569            .unwrap();
570
571        adapt_request_version(&mut req, Version::HTTP_3).unwrap();
572
573        // same semantic class (h2<->h3): only the version field changes, no cookie
574        // merge, nothing stripped
575        assert_eq!(req.version(), Version::HTTP_3);
576        assert_eq!(req.headers().get_all(COOKIE).iter().count(), 2);
577    }
578
579    #[test]
580    fn test_h1_to_h2_unsupported_upgrade_errors() {
581        let mut req = Request::builder()
582            .version(Version::HTTP_11)
583            .method(Method::GET)
584            .uri("https://example.com/")
585            .header(UPGRADE, "myproto")
586            .header(CONNECTION, "Upgrade")
587            .body(())
588            .unwrap();
589
590        // a genuine non-websocket upgrade switch we can't translate -> explicit error,
591        // not a silent strip.
592        let err = adapt_request_version(&mut req, Version::HTTP_2).unwrap_err();
593        assert!(
594            err.to_string().contains("only websocket is supported"),
595            "{err}"
596        );
597    }
598
599    #[test]
600    fn test_h1_to_h2_upgrade_advertisement_is_not_a_switch() {
601        let mut req = Request::builder()
602            .version(Version::HTTP_11)
603            .method(Method::GET)
604            .uri("https://example.com/")
605            // `Upgrade` without `Connection: Upgrade` is a mere advertisement, not a
606            // protocol switch -> no error, just stripped on the way to h2.
607            .header(UPGRADE, "h2c")
608            .body(())
609            .unwrap();
610
611        adapt_request_version(&mut req, Version::HTTP_2).unwrap();
612
613        assert_eq!(req.version(), Version::HTTP_2);
614        assert!(!req.headers().contains_key(UPGRADE));
615    }
616
617    #[test]
618    fn test_h2_to_h1_unsupported_extended_connect_errors() {
619        let mut req = Request::builder()
620            .version(Version::HTTP_2)
621            .method(Method::CONNECT)
622            .uri("https://example.com/")
623            .body(())
624            .unwrap();
625        req.extensions()
626            .insert(Protocol::from_static("connect-udp"));
627
628        let err = adapt_request_version(&mut req, Version::HTTP_11).unwrap_err();
629        assert!(
630            err.to_string().contains("only websocket is supported"),
631            "{err}"
632        );
633    }
634
635    #[test]
636    fn test_merge_multiple_cookies_http2_to_http1() {
637        let mut req = Request::builder()
638            .version(Version::HTTP_2)
639            .uri("https://example.com")
640            .header(COOKIE, "a=1")
641            .header(COOKIE, "b=2")
642            .header(COOKIE, "c=3")
643            .body(())
644            .unwrap();
645
646        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
647
648        // Should now have exactly one Cookie header
649        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
650        assert_eq!(
651            cookie_values.len(),
652            1,
653            "Should have exactly one Cookie header"
654        );
655
656        // The merged value should contain all cookies joined by "; "
657        assert_eq!(cookie_values[0].as_bytes(), b"a=1; b=2; c=3");
658
659        // Version should be changed
660        assert_eq!(req.version(), Version::HTTP_11);
661    }
662
663    #[test]
664    fn test_merge_multiple_cookies_http3_to_http1() {
665        let mut req = Request::builder()
666            .version(Version::HTTP_3)
667            .uri("https://example.com")
668            .header(COOKIE, "session=abc123")
669            .header(COOKIE, "token=xyz789")
670            .body(())
671            .unwrap();
672
673        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
674
675        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
676        assert_eq!(
677            cookie_values.len(),
678            1,
679            "Should have exactly one Cookie header"
680        );
681        assert_eq!(cookie_values[0].as_bytes(), b"session=abc123; token=xyz789");
682    }
683
684    #[test]
685    fn test_single_cookie_http2_to_http1_unchanged() {
686        let mut req = Request::builder()
687            .version(Version::HTTP_2)
688            .uri("https://example.com")
689            .header(COOKIE, "single=cookie")
690            .body(())
691            .unwrap();
692
693        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
694
695        // Should still have one Cookie header, unchanged
696        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
697        assert_eq!(cookie_values.len(), 1);
698        assert_eq!(cookie_values[0].as_bytes(), b"single=cookie");
699    }
700
701    #[test]
702    fn test_no_merge_http1_to_http2() {
703        let mut req = Request::builder()
704            .version(Version::HTTP_11)
705            .uri("https://example.com")
706            .header(COOKIE, "a=1")
707            .header(COOKIE, "b=2")
708            .body(())
709            .unwrap();
710
711        adapt_request_version(&mut req, Version::HTTP_2).unwrap();
712
713        // When going from HTTP/1 to HTTP/2, don't merge (keep as-is)
714        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
715        assert_eq!(
716            cookie_values.len(),
717            2,
718            "Should preserve multiple headers when converting to HTTP/2"
719        );
720    }
721
722    #[test]
723    fn test_no_cookies_http2_to_http1() {
724        let mut req = Request::builder()
725            .version(Version::HTTP_2)
726            .uri("https://example.com")
727            .body(())
728            .unwrap();
729
730        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
731
732        // Should have no Cookie headers
733        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
734        assert_eq!(cookie_values.len(), 0);
735    }
736
737    #[test]
738    fn test_merge_preserves_order() {
739        let mut req = Request::builder()
740            .version(Version::HTTP_2)
741            .uri("https://example.com")
742            .header(COOKIE, "first=1")
743            .header(COOKIE, "second=2")
744            .header(COOKIE, "third=3")
745            .header(COOKIE, "fourth=4")
746            .body(())
747            .unwrap();
748
749        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
750
751        // Should have only one cookie header and the order should be preserved
752        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
753        assert_eq!(cookie_values.len(), 1);
754        assert_eq!(
755            cookie_values[0].as_bytes(),
756            b"first=1; second=2; third=3; fourth=4",
757            "Cookie order should be preserved"
758        );
759    }
760
761    #[test]
762    fn test_complex_cookie_values() {
763        let mut req = Request::builder()
764            .version(Version::HTTP_2)
765            .uri("https://example.com")
766            .header(COOKIE, "uaid=abc123def456")
767            .header(COOKIE, "MSCC=NR")
768            .header(COOKIE, "MUID=1234567890ABCDEF")
769            .header(COOKIE, "VAL1=ASD=DSA&HASH=41&LV=41&V=4&LU=41")
770            .header(COOKIE, "empty=")
771            .body(())
772            .unwrap();
773
774        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
775
776        // also should have only one cookie header with complex values preserved
777        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
778        assert_eq!(cookie_values.len(), 1);
779        assert_eq!(
780            cookie_values[0].as_bytes(),
781            b"uaid=abc123def456; MSCC=NR; MUID=1234567890ABCDEF; VAL1=ASD=DSA&HASH=41&LV=41&V=4&LU=41; empty=",
782        );
783    }
784
785    #[test]
786    fn test_same_version_http2_keeps_multiple_cookies() {
787        let mut req = Request::builder()
788            .version(Version::HTTP_2)
789            .uri("https://example.com")
790            .header(COOKIE, "a=1")
791            .header(COOKIE, "b=2")
792            .body(())
793            .unwrap();
794
795        adapt_request_version(&mut req, Version::HTTP_2).unwrap();
796
797        // multiple Cookie headers are legal in HTTP/2, so normalizing must not merge them
798        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
799        assert_eq!(cookie_values.len(), 2);
800    }
801
802    #[test]
803    fn test_same_version_http1_is_noop() {
804        let mut req = Request::builder()
805            .version(Version::HTTP_11)
806            .uri("https://example.com")
807            .header(COOKIE, "a=1")
808            .header(COOKIE, "b=2")
809            .body(())
810            .unwrap();
811
812        // adapting to the same version is a no-op (it only translates across versions)
813        adapt_request_version(&mut req, Version::HTTP_11).unwrap();
814
815        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
816        assert_eq!(cookie_values.len(), 2);
817    }
818
819    #[test]
820    fn test_ensure_valid_h1_request_normalizes() {
821        let mut req = Request::builder()
822            .version(Version::HTTP_11)
823            .uri("https://example.com")
824            .header(COOKIE, "a=1")
825            .header(COOKIE, "b=2")
826            .body(())
827            .unwrap();
828
829        // the standalone normalizer makes the request valid for HTTP/1: multiple Cookie
830        // headers are merged into one (RFC 6265 §5.4) and a Host header is ensured.
831        ensure_valid_request_for_version(&mut req).unwrap();
832
833        let cookie_values: Vec<_> = req.headers().get_all(COOKIE).iter().collect();
834        assert_eq!(cookie_values.len(), 1);
835        assert_eq!(cookie_values[0].as_bytes(), b"a=1; b=2");
836        assert!(req.headers().contains_key(HOST));
837    }
838}