Skip to main content

rama_http/layer/version_adapter/
request.rs

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