Skip to main content

rama_http/layer/version_adapter/
response.rs

1use rama_core::error::{BoxError, ErrorContext as _};
2use rama_core::telemetry::tracing;
3use rama_core::{Layer, Service};
4use rama_http_headers::{Connection, HeaderMapExt, SecWebSocketAccept, SecWebSocketKey, Upgrade};
5use rama_http_types::header::SEC_WEBSOCKET_ACCEPT;
6use rama_http_types::proto::h2::ext::Protocol;
7use rama_http_types::{Request, Response, StatusCode, Version};
8
9use super::request::{is_websocket_protocol, request_connect_protocol};
10use crate::layer::remove_header::remove_illegal_h2_response_headers;
11
12#[derive(Clone, Debug)]
13/// [`Service`] which will adapt the response version to the original request version.
14///
15/// When a request passes through this [`Service`] it will store the request version,
16/// and if the response has a different [`Version`] it will adapt it back the original one.
17///
18/// Warning: when used together with a [`RequestVersionAdapter`] make sure this is placed
19/// first so it can store the info it needs from the original response in a [`ResponseVersionAdaptCtx`]
20///
21/// [`RequestVersionAdapter`]: super::RequestVersionAdapter
22pub struct ResponseVersionAdapter<S> {
23    inner: S,
24}
25
26impl<S> ResponseVersionAdapter<S> {
27    pub fn new(inner: S) -> Self {
28        Self { inner }
29    }
30}
31
32impl<S, Body> Service<Request<Body>> for ResponseVersionAdapter<S>
33where
34    S: Service<Request<Body>, Output = Response, Error: Into<BoxError>>,
35    Body: Send + 'static,
36{
37    type Output = S::Output;
38    type Error = BoxError;
39
40    async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
41        let request_ctx = ResponseVersionAdaptCtx::from_request(&req);
42
43        let mut resp = self.inner.serve(req).await.into_box_error()?;
44        adapt_response_version(&mut resp, &request_ctx)?;
45
46        Ok(resp)
47    }
48}
49
50#[non_exhaustive]
51#[derive(Clone, Debug, Default)]
52/// [`Layer`] which will adapt the response version to the original request version.
53///
54/// When a request passes through this [`Layer`] it will store the request version,
55/// and if the response has a different [`Version`] it will adapt it back the original one.
56pub struct ResponseVersionAdapterLayer;
57
58impl<S> Layer<S> for ResponseVersionAdapterLayer {
59    type Service = ResponseVersionAdapter<S>;
60
61    fn layer(&self, inner: S) -> Self::Service {
62        ResponseVersionAdapter { inner }
63    }
64}
65
66/// Request-derived state, captured before a request is sent, that a response may
67/// need in order to be translated back to the original request [`Version`].
68///
69/// The motivating case is the WebSocket handshake: an HTTP/1 `101 Switching
70/// Protocols` accept carries `Sec-WebSocket-Accept`, which is a signature of the
71/// request's `Sec-WebSocket-Key`. That key lives only on the request, so it cannot be
72/// reconstructed from an HTTP/2 `200` response alone, it must be captured here.
73#[derive(Debug, Clone, Default)]
74pub struct ResponseVersionAdaptCtx {
75    ///  Original version the request had
76    pub version: Version,
77    /// The Extended CONNECT / `Upgrade` application protocol the request asked for
78    /// (e.g. `websocket`), if any. Captured from the request's `:protocol` extension
79    /// (HTTP/2/3 CONNECT) or `Upgrade` header (HTTP/1).
80    pub connect_protocol: Option<Protocol>,
81    /// The request's `Sec-WebSocket-Key`, needed to recompute `Sec-WebSocket-Accept`
82    /// when reconstructing an HTTP/1 WebSocket handshake response.
83    pub websocket_key: Option<SecWebSocketKey>,
84}
85
86impl ResponseVersionAdaptCtx {
87    /// Capture the [`ResponseVersionAdaptCtx`] from an outgoing request.
88    pub fn from_request<Body>(request: &Request<Body>) -> Self {
89        Self {
90            version: request.version(),
91            connect_protocol: request_connect_protocol(request),
92            websocket_key: request.headers().typed_get::<SecWebSocketKey>(),
93        }
94    }
95
96    fn is_websocket(&self) -> bool {
97        self.connect_protocol
98            .as_ref()
99            .is_some_and(is_websocket_protocol)
100    }
101}
102
103/// Adapt response to match the provided [`Version`], using [`ResponseVersionAdaptCtx`]
104/// captured from the original request to translate handshake responses.
105pub fn adapt_response_version<Body>(
106    response: &mut Response<Body>,
107    request_ctx: &ResponseVersionAdaptCtx,
108) -> Result<(), BoxError> {
109    let resp_version = response.version();
110    if resp_version == request_ctx.version {
111        tracing::trace!(
112            version = ?response.version(),
113            "response version is already correct, no version switching needed",
114        );
115        return Ok(());
116    }
117
118    tracing::trace!(
119        ?resp_version,
120        target_version = ?request_ctx.version,
121        "changing response version",
122    );
123
124    // HTTP/2 and HTTP/3 share the same semantic model (no connection-specific headers,
125    // Extended CONNECT for upgrades), so translation keys on the h1-style (`<= HTTP_11`)
126    // vs modern (`>= HTTP_2`) class transition. This makes HTTP/3 work here for free.
127    let resp_is_h1 = resp_version <= Version::HTTP_11;
128    let target_is_h1 = request_ctx.version <= Version::HTTP_11;
129
130    match (resp_is_h1, target_is_h1) {
131        (true, false) => upgrade_response_to_h2_or_h3(response, request_ctx)?,
132        (false, true) => downgrade_response_to_h1(response, request_ctx)?,
133        // same class (HTTP/1.0<->1.1 or HTTP/2<->HTTP/3): only the version field changes
134        (true, true) | (false, false) => {}
135    }
136
137    *response.version_mut() = request_ctx.version;
138    Ok(())
139}
140
141/// Translate an HTTP/1.x response up to HTTP/2 or HTTP/3.
142///
143/// For a WebSocket handshake (known from the captured request context) the HTTP/1
144/// `101 Switching Protocols` accept becomes the `200 OK` form (RFC 8441 §5.1).
145/// Otherwise removes the connection-specific headers that are *illegal* in HTTP/2 and
146/// HTTP/3 (RFC 9113 §8.2.2 / RFC 9114 §4.2) so an ordinary response can be
147/// (re)serialized over the binary-framed protocol.
148///
149/// A `101` whose captured request protocol is NOT websocket (h2c, WebTransport, …) has
150/// no supported modern translation and is rejected with an error rather than silently
151/// mishandled. (The actual byte-stream bridging of an upgrade across versions is always
152/// the upgrade/relay layer's job — the adapter only translates the handshake status.)
153fn upgrade_response_to_h2_or_h3<Body>(
154    response: &mut Response<Body>,
155    request_ctx: &ResponseVersionAdaptCtx,
156) -> Result<(), BoxError> {
157    if response.status() == StatusCode::SWITCHING_PROTOCOLS {
158        if request_ctx.is_websocket() {
159            tracing::trace!("translating h1 websocket 101 response into h2/h3 200 OK");
160            *response.status_mut() = StatusCode::OK;
161            // `Sec-WebSocket-Accept` is unused in h2/h3; drop it (the connection-specific
162            // upgrade headers are removed by the illegal-header strip below).
163            response.headers_mut().remove(SEC_WEBSOCKET_ACCEPT);
164        } else {
165            return Err(BoxError::from(format!(
166                "cannot translate a `101 Switching Protocols` response to HTTP/2+ for protocol {}: only websocket is supported",
167                request_ctx
168                    .connect_protocol
169                    .as_ref()
170                    .map_or("<unknown upgrade>", Protocol::as_str),
171            )));
172        }
173    }
174
175    // Remove only the connection-specific headers that are *illegal* in HTTP/2 & HTTP/3.
176    // This is a protocol-legality fixup, not a proxy hop policy: headers that are legal
177    // in those versions (e.g. `Trailer`, `Proxy-Authenticate`) are left untouched.
178    remove_illegal_h2_response_headers(response.headers_mut());
179    Ok(())
180}
181
182/// Translate an HTTP/2 or HTTP/3 response down to HTTP/1.x.
183///
184/// For a WebSocket handshake (known from the captured request context) the Extended
185/// CONNECT `200 OK` becomes the HTTP/1 `101 Switching Protocols` accept, re-deriving
186/// `Sec-WebSocket-Accept` from the request's `Sec-WebSocket-Key`. Ordinary responses
187/// (no captured Extended CONNECT protocol) carry no connection-specific headers and
188/// need only the version swap. A non-WebSocket Extended CONNECT is rejected.
189fn downgrade_response_to_h1<Body>(
190    response: &mut Response<Body>,
191    request_ctx: &ResponseVersionAdaptCtx,
192) -> Result<(), BoxError> {
193    let Some(protocol) = request_ctx.connect_protocol.as_ref() else {
194        // Not an Extended CONNECT request: an ordinary response needs only the version swap.
195        return Ok(());
196    };
197    if !is_websocket_protocol(protocol) {
198        return Err(BoxError::from(format!(
199            "cannot translate an Extended CONNECT `{}` response to HTTP/1: only websocket is supported",
200            protocol.as_str(),
201        )));
202    }
203
204    // A WebSocket success (`200`) becomes the HTTP/1 `101` accept; a non-success
205    // response (handshake rejected upstream) passes through apart from the version.
206    if response.status() == StatusCode::OK {
207        tracing::trace!("translating h2/h3 websocket 200 response into h1 101 Switching Protocols");
208        *response.status_mut() = StatusCode::SWITCHING_PROTOCOLS;
209
210        let headers = response.headers_mut();
211        headers.typed_insert(Upgrade::websocket());
212        headers.typed_insert(Connection::upgrade());
213        if let Some(key) = request_ctx.websocket_key.clone() {
214            let accept = SecWebSocketAccept::try_from(key)
215                .context("derive Sec-WebSocket-Accept for h1 websocket handshake response")?;
216            headers.typed_insert(accept);
217        } else {
218            tracing::debug!(
219                "no Sec-WebSocket-Key captured; emitting h1 websocket 101 without Sec-WebSocket-Accept",
220            );
221        }
222    }
223    Ok(())
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use rama_core::extensions::ExtensionsRef;
230    use rama_http_types::Method;
231    use rama_http_types::header::{CONNECTION, SEC_WEBSOCKET_KEY, TRANSFER_ENCODING, UPGRADE};
232
233    const SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ==";
234    // RFC 6455 §1.3 worked example: accept for the key above.
235    const SAMPLE_ACCEPT: &str = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
236
237    fn ctx_with_version(version: Version) -> ResponseVersionAdaptCtx {
238        ResponseVersionAdaptCtx {
239            version,
240            ..Default::default()
241        }
242    }
243
244    fn websocket_ctx(version: Version) -> ResponseVersionAdaptCtx {
245        let req = Request::builder()
246            .version(version)
247            .uri("https://example.com/chat")
248            .header(UPGRADE, "websocket")
249            .header(CONNECTION, "Upgrade")
250            .header(SEC_WEBSOCKET_KEY, SAMPLE_KEY)
251            .body(())
252            .unwrap();
253        ResponseVersionAdaptCtx::from_request(&req)
254    }
255
256    fn connect_udp_ctx(version: Version) -> ResponseVersionAdaptCtx {
257        let req = Request::builder()
258            .version(version)
259            .method(Method::CONNECT)
260            .uri("https://example.com/.well-known/masque/udp/1.2.3.4/443/")
261            .body(())
262            .unwrap();
263        req.extensions()
264            .insert(Protocol::from_static("connect-udp"));
265        ResponseVersionAdaptCtx::from_request(&req)
266    }
267
268    #[test]
269    fn test_h1_to_h2_strips_hop_by_hop_headers() {
270        let mut resp = Response::builder()
271            .version(Version::HTTP_11)
272            .header(CONNECTION, "keep-alive")
273            .header("keep-alive", "timeout=5")
274            .header(TRANSFER_ENCODING, "chunked")
275            .header("content-type", "text/plain")
276            // legal in HTTP/2 — must be preserved (not a protocol-illegal header)
277            .header("trailer", "expires")
278            .header("proxy-authenticate", "Basic")
279            .body(())
280            .unwrap();
281
282        adapt_response_version(&mut resp, &ctx_with_version(Version::HTTP_2)).unwrap();
283
284        assert_eq!(resp.version(), Version::HTTP_2);
285        assert!(!resp.headers().contains_key(CONNECTION));
286        assert!(!resp.headers().contains_key("keep-alive"));
287        assert!(!resp.headers().contains_key(TRANSFER_ENCODING));
288        assert_eq!(resp.headers().get("content-type").unwrap(), "text/plain");
289        // legal-in-HTTP/2 headers survive a pure version change
290        assert_eq!(resp.headers().get("trailer").unwrap(), "expires");
291        assert_eq!(resp.headers().get("proxy-authenticate").unwrap(), "Basic");
292    }
293
294    #[test]
295    fn test_h1_to_h2_non_websocket_101_errors() {
296        let mut resp = Response::builder()
297            .version(Version::HTTP_11)
298            .status(StatusCode::SWITCHING_PROTOCOLS)
299            .header(UPGRADE, "h2c")
300            .header(CONNECTION, "Upgrade")
301            .body(())
302            .unwrap();
303
304        // a non-websocket 101 has no supported HTTP/2 translation -> explicit error,
305        // not a silent passthrough.
306        let err =
307            adapt_response_version(&mut resp, &ctx_with_version(Version::HTTP_2)).unwrap_err();
308        assert!(
309            err.to_string().contains("only websocket is supported"),
310            "{err}"
311        );
312    }
313
314    #[test]
315    fn test_h2_to_h1_unsupported_extended_connect_errors() {
316        let mut resp = Response::builder()
317            .version(Version::HTTP_2)
318            .status(StatusCode::OK)
319            .body(())
320            .unwrap();
321
322        // a non-websocket Extended CONNECT (e.g. connect-udp) cannot be downgraded to
323        // an HTTP/1 handshake -> explicit error.
324        let err =
325            adapt_response_version(&mut resp, &connect_udp_ctx(Version::HTTP_11)).unwrap_err();
326        assert!(
327            err.to_string().contains("only websocket is supported"),
328            "{err}"
329        );
330    }
331
332    #[test]
333    fn test_h1_to_h2_websocket_101_becomes_200() {
334        let mut resp = Response::builder()
335            .version(Version::HTTP_11)
336            .status(StatusCode::SWITCHING_PROTOCOLS)
337            .header(UPGRADE, "websocket")
338            .header(CONNECTION, "Upgrade")
339            .header("sec-websocket-accept", SAMPLE_ACCEPT)
340            .body(())
341            .unwrap();
342
343        adapt_response_version(&mut resp, &websocket_ctx(Version::HTTP_2)).unwrap();
344
345        assert_eq!(resp.status(), StatusCode::OK);
346        assert_eq!(resp.version(), Version::HTTP_2);
347        // h1 handshake / connection-specific headers are gone in h2
348        assert!(!resp.headers().contains_key(UPGRADE));
349        assert!(!resp.headers().contains_key(CONNECTION));
350        assert!(!resp.headers().contains_key("sec-websocket-accept"));
351    }
352
353    #[test]
354    fn test_h2_to_h1_websocket_200_becomes_101() {
355        let mut resp = Response::builder()
356            .version(Version::HTTP_2)
357            .status(StatusCode::OK)
358            .body(())
359            .unwrap();
360
361        adapt_response_version(&mut resp, &websocket_ctx(Version::HTTP_11)).unwrap();
362
363        assert_eq!(resp.version(), Version::HTTP_11);
364        assert_eq!(resp.status(), StatusCode::SWITCHING_PROTOCOLS);
365        assert!(
366            resp.headers()
367                .typed_get::<Upgrade>()
368                .is_some_and(|u| u.is_websocket())
369        );
370        assert!(
371            resp.headers()
372                .typed_get::<Connection>()
373                .is_some_and(|c| c.contains_upgrade())
374        );
375        // the accept is recomputed from the original request key (RFC 6455 example)
376        assert_eq!(
377            resp.headers().get("sec-websocket-accept").unwrap(),
378            SAMPLE_ACCEPT,
379        );
380    }
381
382    #[test]
383    fn test_h2_to_h1_non_websocket_only_changes_version() {
384        let mut resp = Response::builder()
385            .version(Version::HTTP_2)
386            .status(StatusCode::OK)
387            .header("content-type", "text/plain")
388            .body(())
389            .unwrap();
390
391        adapt_response_version(&mut resp, &ctx_with_version(Version::HTTP_11)).unwrap();
392
393        assert_eq!(resp.version(), Version::HTTP_11);
394        assert_eq!(resp.status(), StatusCode::OK);
395        assert_eq!(resp.headers().get("content-type").unwrap(), "text/plain");
396    }
397}