Skip to main content

rama_http_backend/client/
connect_request.rs

1use rama_core::{
2    Layer, Service,
3    error::{BoxError, BoxErrorExt as _},
4    extensions::ExtensionsRef,
5};
6use rama_http_types::Request;
7use rama_net::{
8    AuthorityInputExt, ProtocolInputExt, TransportProtocolInputExt,
9    client::{
10        ConnectRequest, ConnectionError, ConnectionErrorKind, ConnectorService,
11        EstablishedClientConnection,
12    },
13    http::HttpRequestVersion,
14};
15use rama_utils::macros::define_inner_service_accessors;
16
17fn try_from_http_request<Body>(request: &Request<Body>) -> Result<ConnectRequest, ConnectionError> {
18    let application_protocol = request.protocol().cloned();
19    let authority = request
20        .authority()
21        .and_then(|authority| {
22            authority.into_host_with_port(
23                application_protocol
24                    .as_ref()
25                    .and_then(|protocol| protocol.default_port()),
26            )
27        })
28        .ok_or_else(|| {
29            ConnectionError::local(
30                BoxError::from_static_str("HTTP request authority is missing a host or port"),
31                ConnectionErrorKind::InvalidInput,
32            )
33            .context("HTTP connect request adapter: derive authority")
34        })?;
35
36    // The adapter and the original request are different logical inputs. Keep
37    // connector mutations isolated until an attempt succeeds, then install the
38    // successful extension chain back on the original request.
39    let extensions = request.extensions().fork();
40    // This is only a plain-connection fallback for HttpConnector. It must not
41    // be a TargetHttpVersion: that would pin TLS ALPN merely because an HTTP
42    // request has an initial version.
43    extensions.insert(HttpRequestVersion(request.version()));
44
45    Ok(ConnectRequest::new_with_extensions(authority, extensions)
46        .maybe_with_application_protocol(application_protocol)
47        .maybe_with_transport_protocol(request.transport_protocol()))
48}
49
50/// Adapt an HTTP [`Request`] to a protocol-independent [`ConnectRequest`].
51///
52/// The original request, including its body, remains owned by this adapter
53/// while the inner connector establishes a connection. The successful
54/// connector input's extension chain is then installed on the original request
55/// so selected routes and other connection metadata remain observable without
56/// requiring the body to be cloned.
57#[derive(Debug, Clone)]
58#[non_exhaustive]
59pub struct HttpConnectRequestAdapter<S> {
60    inner: S,
61}
62
63impl<S> HttpConnectRequestAdapter<S> {
64    /// Create a new [`HttpConnectRequestAdapter`].
65    #[must_use]
66    pub const fn new(inner: S) -> Self {
67        Self { inner }
68    }
69
70    define_inner_service_accessors!();
71}
72
73impl<S, Body> Service<Request<Body>> for HttpConnectRequestAdapter<S>
74where
75    S: ConnectorService<ConnectRequest>,
76    Body: Send + 'static,
77{
78    type Output = EstablishedClientConnection<S::Connection, Request<Body>>;
79    type Error = ConnectionError;
80
81    async fn serve(&self, request: Request<Body>) -> Result<Self::Output, Self::Error> {
82        let connect_request = try_from_http_request(&request)?;
83        let EstablishedClientConnection {
84            conn,
85            input: connect_request,
86        } = Box::pin(self.inner.connect(connect_request)).await?;
87
88        let (mut parts, body) = request.into_parts();
89        parts.extensions = connect_request.extensions;
90
91        Ok(EstablishedClientConnection {
92            conn,
93            input: Request::from_parts(parts, body),
94        })
95    }
96}
97
98/// Layer that adapts HTTP requests to protocol-independent connection inputs.
99#[derive(Debug, Clone, Copy, Default)]
100#[non_exhaustive]
101pub struct HttpConnectRequestAdapterLayer;
102
103impl HttpConnectRequestAdapterLayer {
104    /// Create a new [`HttpConnectRequestAdapterLayer`].
105    #[must_use]
106    pub const fn new() -> Self {
107        Self
108    }
109}
110
111impl<S> Layer<S> for HttpConnectRequestAdapterLayer {
112    type Service = HttpConnectRequestAdapter<S>;
113
114    fn layer(&self, inner: S) -> Self::Service {
115        HttpConnectRequestAdapter::new(inner)
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::convert::Infallible;
122
123    use rama_core::{
124        ServiceInput,
125        extensions::{Extension, ExtensionsRef},
126        service::service_fn,
127    };
128    use rama_http_types::{Method, Request, Version};
129    use rama_net::{
130        Protocol, ProtocolInputExt, TransportProtocolInputExt,
131        client::{ConnectRequest, EstablishedClientConnection},
132        transport::TransportProtocol,
133    };
134
135    use super::*;
136
137    #[derive(Debug)]
138    struct NonCloneBody;
139
140    #[derive(Debug, Extension)]
141    struct RequestMarker;
142
143    #[derive(Debug, Extension)]
144    struct SelectedAttemptMarker;
145
146    #[tokio::test]
147    async fn adapts_without_cloning_body_and_restores_selected_extensions() {
148        let inner = service_fn(async |input: ConnectRequest| {
149            assert_eq!(input.authority.to_string(), "example.com:443");
150            assert_eq!(input.protocol(), Some(&Protocol::HTTPS));
151            assert_eq!(input.transport_protocol(), Some(TransportProtocol::Tcp));
152            assert_eq!(
153                input
154                    .extensions
155                    .get_ref::<HttpRequestVersion>()
156                    .map(|v| v.0),
157                Some(Version::HTTP_2)
158            );
159            assert!(input.extensions.contains::<RequestMarker>());
160            assert!(!input.extensions.self_contains::<RequestMarker>());
161
162            input.extensions.insert(SelectedAttemptMarker);
163            Ok::<_, Infallible>(EstablishedClientConnection {
164                input,
165                conn: ServiceInput::new(()),
166            })
167        });
168        let adapter = HttpConnectRequestAdapter::new(inner);
169
170        let request = Request::builder()
171            .method(Method::POST)
172            .uri("https://example.com/upload")
173            .version(Version::HTTP_2)
174            .body(NonCloneBody)
175            .unwrap();
176        request.extensions().insert(RequestMarker);
177
178        let established = adapter.serve(request).await.unwrap();
179        assert_eq!(established.input.method(), Method::POST);
180        assert_eq!(established.input.uri().path_or_root().as_ref(), "/upload");
181        assert!(established.input.extensions().contains::<RequestMarker>());
182        assert!(
183            established
184                .input
185                .extensions()
186                .contains::<SelectedAttemptMarker>()
187        );
188    }
189
190    #[tokio::test]
191    async fn rejects_request_without_resolvable_authority() {
192        let inner = service_fn(async |input: ConnectRequest| {
193            Ok::<_, Infallible>(EstablishedClientConnection {
194                input,
195                conn: ServiceInput::new(()),
196            })
197        });
198        let adapter = HttpConnectRequestAdapter::new(inner);
199        let request = Request::builder().uri("/").body(NonCloneBody).unwrap();
200
201        let error = adapter.serve(request).await.unwrap_err();
202        assert_eq!(
203            error.domain(),
204            rama_net::client::ConnectionErrorDomain::Local
205        );
206        assert_eq!(error.kind(), ConnectionErrorKind::InvalidInput);
207    }
208}