Skip to main content

wasmtime_wasi_http/p3/
request.rs

1use crate::p3::bindings::http::types::ErrorCode;
2use crate::p3::body::{Body, BodyExt as _, GuestBody};
3use crate::p3::{HttpError, HttpResult};
4use crate::{
5    Error, FieldMap, RequestOptions, WasiHttpCtxView, WasiHttpHooks, WasiHttpView,
6    get_content_length,
7};
8use bytes::Bytes;
9use http::header::HOST;
10use http::uri::{Authority, PathAndQuery, Scheme};
11use http::{HeaderValue, Method, Uri};
12use http_body_util::BodyExt as _;
13use http_body_util::combinators::UnsyncBoxBody;
14use std::sync::Arc;
15use tokio::sync::oneshot;
16use tracing::debug;
17use wasmtime::AsContextMut;
18
19/// The concrete type behind a `wasi:http/types.request` resource.
20pub struct Request {
21    /// The method of the request.
22    pub method: Method,
23    /// The scheme of the request.
24    pub scheme: Option<Scheme>,
25    /// The authority of the request.
26    pub authority: Option<Authority>,
27    /// The path and query of the request.
28    pub path_with_query: Option<PathAndQuery>,
29    /// The request headers.
30    pub headers: FieldMap,
31    /// Request options.
32    pub options: Option<Arc<RequestOptions>>,
33    /// Request body.
34    pub(crate) body: Body,
35}
36
37impl Request {
38    /// Construct a new [Request]
39    ///
40    /// This returns a [Future] that the will be used to communicate
41    /// a request processing error, if any.
42    ///
43    /// Requests constructed this way will not perform any `Content-Length` validation.
44    pub fn new<B>(
45        method: Method,
46        scheme: Option<Scheme>,
47        authority: Option<Authority>,
48        path_with_query: Option<PathAndQuery>,
49        headers: impl Into<FieldMap>,
50        options: Option<Arc<RequestOptions>>,
51        body: B,
52    ) -> (
53        Self,
54        impl Future<Output = Result<(), Error>> + Send + 'static,
55    )
56    where
57        B: http_body::Body<Data = Bytes> + Send + 'static,
58        B::Error: Into<Error>,
59    {
60        let (tx, rx) = oneshot::channel();
61        (
62            Self {
63                method,
64                scheme,
65                authority,
66                path_with_query,
67                headers: headers.into(),
68                options,
69                body: Body::Host {
70                    body: body.map_err(Into::into).boxed_unsync(),
71                    result_tx: tx,
72                },
73            },
74            async {
75                let Ok(fut) = rx.await else { return Ok(()) };
76                Box::into_pin(fut).await
77            },
78        )
79    }
80
81    /// Construct a new [Request] from [http::Request].
82    ///
83    /// This returns a [Future] that will be used to communicate
84    /// a request processing error, if any.
85    ///
86    /// Requests constructed this way will not perform any `Content-Length` validation.
87    pub fn from_http<T>(
88        hooks: &mut dyn WasiHttpHooks,
89        req: http::Request<T>,
90    ) -> (
91        Self,
92        impl Future<Output = Result<(), Error>> + Send + 'static + use<T>,
93    )
94    where
95        T: http_body::Body<Data = Bytes> + Send + 'static,
96        T::Error: Into<Error>,
97    {
98        let (
99            http::request::Parts {
100                method,
101                uri,
102                headers,
103                ..
104            },
105            body,
106        ) = req.into_parts();
107        let http::uri::Parts {
108            scheme,
109            authority,
110            path_and_query,
111            ..
112        } = uri.into_parts();
113        Self::new(
114            method,
115            scheme,
116            authority,
117            path_and_query,
118            FieldMap::new_immutable(hooks, headers),
119            None,
120            body,
121        )
122    }
123
124    /// Convert this [`Request`] into an [`http::Request<UnsyncBoxBody<Bytes, ErrorCode>>`].
125    ///
126    /// The specified future `fut` can be used to communicate a request processing
127    /// error, if any, back to the caller (e.g., if this request was constructed
128    /// through `wasi:http/types.request#new`).
129    pub fn into_http<T: WasiHttpView + 'static>(
130        self,
131        store: impl AsContextMut<Data = T>,
132        fut: impl Future<Output = Result<(), Error>> + Send + 'static,
133    ) -> HttpResult<(
134        http::Request<UnsyncBoxBody<Bytes, Error>>,
135        Option<Arc<RequestOptions>>,
136    )> {
137        self.into_http_with_getter(store, fut, T::http)
138    }
139
140    /// Like [`Self::into_http`], but uses a custom getter for obtaining the [`WasiHttpCtxView`].
141    pub fn into_http_with_getter<T: 'static>(
142        self,
143        mut store: impl AsContextMut<Data = T>,
144        fut: impl Future<Output = Result<(), Error>> + Send + 'static,
145        mut getter: impl FnMut(&mut T) -> WasiHttpCtxView<'_> + Clone + Unpin + Send + 'static,
146    ) -> HttpResult<(
147        http::Request<UnsyncBoxBody<Bytes, Error>>,
148        Option<Arc<RequestOptions>>,
149    )> {
150        let Request {
151            method,
152            scheme,
153            authority,
154            path_with_query,
155            mut headers,
156            options,
157            body,
158        } = self;
159        // `Content-Length` header value is validated in `fields` implementation
160        let content_length = match get_content_length(&headers) {
161            Ok(content_length) => content_length,
162            Err(err) => {
163                body.drop(&mut store).map_err(HttpError::trap)?;
164                return Err(ErrorCode::InternalError(Some(format!("{err:#}"))).into());
165            }
166        };
167        // This match must appear before any potential errors handled with '?'
168        // (or errors have to explicitly be addressed and drop the body, as above),
169        // as otherwise the Body::Guest resources will not be cleaned up when dropped.
170        // see: https://github.com/bytecodealliance/wasmtime/pull/11440#discussion_r2326139381
171        // for additional context.
172        let body = match body {
173            Body::Guest {
174                contents_rx,
175                trailers_rx,
176                result_tx,
177            } => GuestBody::new(
178                &mut store,
179                contents_rx,
180                trailers_rx,
181                result_tx,
182                fut,
183                content_length,
184                ErrorCode::HttpRequestBodySize,
185                getter.clone(),
186            )
187            .map_err(HttpError::trap)?
188            .boxed_unsync(),
189            Body::Host { body, result_tx } => {
190                let is_forbidden = getter(store.as_context_mut().data_mut())
191                    .hooks
192                    .forbidden_header_filter();
193                let body = body.with_filtered_trailers(is_forbidden).boxed_unsync();
194                if let Some(limit) = content_length {
195                    let (http_result_tx, http_result_rx) = oneshot::channel();
196                    _ = result_tx.send(Box::new(async move {
197                        if let Ok(err) = http_result_rx.await {
198                            return Err(err);
199                        };
200                        fut.await
201                    }));
202                    body.with_content_length(limit, http_result_tx, Error::HttpRequestBodySize)
203                        .boxed_unsync()
204                } else {
205                    _ = result_tx.send(Box::new(fut));
206                    body
207                }
208            }
209        };
210        let mut store = store.as_context_mut();
211        let WasiHttpCtxView { hooks, ctx, .. } = getter(store.data_mut());
212        headers.set_mutable(ctx.field_size_limit);
213        if hooks.set_host_header() {
214            let host = if let Some(authority) = authority.as_ref() {
215                HeaderValue::try_from(authority.as_str())
216                    .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?
217            } else {
218                HeaderValue::from_static("")
219            };
220            headers.append_raw(HOST, host).map_err(HttpError::trap)?;
221        }
222        let scheme = match scheme {
223            None => hooks.default_scheme().ok_or(ErrorCode::HttpProtocolError)?,
224            Some(scheme) if hooks.is_supported_scheme(&scheme) => scheme,
225            Some(..) => return Err(ErrorCode::HttpProtocolError.into()),
226        };
227        let mut uri = Uri::builder().scheme(scheme);
228        if let Some(authority) = authority {
229            uri = uri.authority(authority)
230        };
231        if let Some(path_with_query) = path_with_query {
232            uri = uri.path_and_query(path_with_query)
233        };
234        let uri = uri.build().map_err(|err| {
235            debug!(?err, "failed to build request URI");
236            ErrorCode::HttpRequestUriInvalid
237        })?;
238        let mut req = http::Request::builder();
239        *req.headers_mut().unwrap() = headers.into();
240        let req = req
241            .method(method)
242            .uri(uri)
243            .body(body)
244            .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?;
245        Ok((req, options))
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::WasiHttpCtx;
253    use core::future::Future;
254    use core::pin::pin;
255    use core::str::FromStr;
256    use core::task::{Context, Poll, Waker};
257    use http_body_util::{BodyExt, Empty, Full};
258    use wasmtime::Result;
259    use wasmtime::{Engine, Store};
260    use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
261
262    struct TestCtx {
263        table: ResourceTable,
264        wasi: WasiCtx,
265        http: WasiHttpCtx,
266    }
267
268    impl TestCtx {
269        fn new() -> Self {
270            Self {
271                table: ResourceTable::default(),
272                wasi: WasiCtxBuilder::new().build(),
273                http: Default::default(),
274            }
275        }
276    }
277
278    impl WasiView for TestCtx {
279        fn ctx(&mut self) -> WasiCtxView<'_> {
280            WasiCtxView {
281                ctx: &mut self.wasi,
282                table: &mut self.table,
283            }
284        }
285    }
286
287    impl WasiHttpView for TestCtx {
288        fn http(&mut self) -> WasiHttpCtxView<'_> {
289            WasiHttpCtxView {
290                ctx: &mut self.http,
291                table: &mut self.table,
292                hooks: crate::default_hooks(),
293            }
294        }
295    }
296
297    #[tokio::test]
298    async fn test_request_into_http_schemes() -> Result<()> {
299        let schemes = vec![Some(Scheme::HTTP), Some(Scheme::HTTPS), None];
300        let engine = Engine::default();
301
302        for scheme in schemes {
303            let (req, fut) = Request::new(
304                Method::POST,
305                scheme.clone(),
306                Some(Authority::from_static("example.com")),
307                Some(PathAndQuery::from_static("/path?query=1")),
308                FieldMap::default(),
309                None,
310                Full::new(Bytes::from_static(b"body")).boxed_unsync(),
311            );
312            let mut store = Store::new(&engine, TestCtx::new());
313            let (http_req, options) = req.into_http(&mut store, async { Ok(()) }).unwrap();
314            assert_eq!(options, None);
315            assert_eq!(http_req.method(), Method::POST);
316            let expected_scheme = scheme.unwrap_or(Scheme::HTTPS); // default scheme
317            assert_eq!(
318                http_req.uri(),
319                &http::Uri::from_str(&format!(
320                    "{}://example.com/path?query=1",
321                    expected_scheme.as_str()
322                ))
323                .unwrap()
324            );
325            let body_bytes = http_req.into_body().collect().await?;
326            assert_eq!(body_bytes.to_bytes(), b"body".as_slice());
327            let mut cx = Context::from_waker(Waker::noop());
328            let result = pin!(fut).poll(&mut cx);
329            assert!(matches!(result, Poll::Ready(Ok(()))));
330        }
331
332        Ok(())
333    }
334
335    #[tokio::test]
336    async fn test_request_into_http_uri_error() -> Result<()> {
337        let (req, fut) = Request::new(
338            Method::GET,
339            Some(Scheme::HTTP),
340            Some(Authority::from_static("example.com")),
341            None, // <-- should fail, must be Some(_) when authority is set
342            FieldMap::default(),
343            None,
344            Empty::new().boxed_unsync(),
345        );
346        let mut store = Store::new(&Engine::default(), TestCtx::new());
347        let result = req
348            .into_http(&mut store, async {
349                Err(Error::InternalError(Some("uh oh".to_string())))
350            })
351            .unwrap_err();
352        assert!(matches!(
353            result.downcast()?,
354            ErrorCode::HttpRequestUriInvalid,
355        ));
356        let mut cx = Context::from_waker(Waker::noop());
357        let result = pin!(fut).poll(&mut cx);
358        assert!(matches!(
359            result,
360            Poll::Ready(Err(Error::InternalError(Some(_))))
361        ));
362
363        Ok(())
364    }
365}