Skip to main content

wasmtime_wasi_http/p3/
response.rs

1use crate::p3::bindings::http::types::ErrorCode;
2use crate::p3::body::{Body, GuestBody};
3use crate::{Error, FieldMap, WasiHttpCtxView, WasiHttpHooks, WasiHttpView, get_content_length};
4use bytes::Bytes;
5use http::StatusCode;
6use http_body_util::BodyExt as _;
7use http_body_util::combinators::UnsyncBoxBody;
8use wasmtime::AsContextMut;
9use wasmtime::error::Context as _;
10
11/// The concrete type behind a `wasi:http/types.response` resource.
12pub struct Response {
13    /// The status of the response.
14    pub status: StatusCode,
15    /// The headers of the response.
16    pub headers: FieldMap,
17    /// Response body.
18    pub(crate) body: Body,
19}
20
21impl TryFrom<Response> for http::Response<Body> {
22    type Error = http::Error;
23
24    fn try_from(
25        Response {
26            status,
27            headers,
28            body,
29        }: Response,
30    ) -> Result<Self, Self::Error> {
31        let mut res = http::Response::builder().status(status);
32        *res.headers_mut().unwrap() = headers.into();
33        res.body(body)
34    }
35}
36
37impl Response {
38    /// Convert [Response] into [http::Response].
39    ///
40    /// The specified [Future] `fut` can be used to communicate
41    /// a response processing error, if any, to the constructor of the response.
42    /// For example, if the response was constructed via `wasi:http/types.response#new`,
43    /// a result sent on `fut` will be forwarded to the guest on the future handle returned.
44    pub fn into_http<T: WasiHttpView + 'static>(
45        self,
46        store: impl AsContextMut<Data = T>,
47        fut: impl Future<Output = Result<(), Error>> + Send + 'static,
48    ) -> wasmtime::Result<http::Response<UnsyncBoxBody<Bytes, Error>>> {
49        self.into_http_with_getter(store, fut, T::http)
50    }
51
52    /// Like [`Self::into_http`], but with a custom function for converting `T`
53    /// to a [`WasiHttpCtxView`].
54    pub fn into_http_with_getter<T: 'static>(
55        self,
56        store: impl AsContextMut<Data = T>,
57        fut: impl Future<Output = Result<(), Error>> + Send + 'static,
58        getter: fn(&mut T) -> WasiHttpCtxView<'_>,
59    ) -> wasmtime::Result<http::Response<UnsyncBoxBody<Bytes, Error>>> {
60        let res = http::Response::try_from(self)?;
61        let (res, body) = res.into_parts();
62        let body = match body {
63            Body::Guest {
64                contents_rx,
65                trailers_rx,
66                result_tx,
67            } => {
68                // `Content-Length` header value is validated in `fields` implementation
69                let content_length =
70                    get_content_length(&res.headers).context("failed to parse `content-length`")?;
71                GuestBody::new(
72                    store,
73                    contents_rx,
74                    trailers_rx,
75                    result_tx,
76                    fut,
77                    content_length,
78                    ErrorCode::HttpResponseBodySize,
79                    getter,
80                )?
81                .boxed_unsync()
82            }
83            Body::Host { body, result_tx } => {
84                _ = result_tx.send(Box::new(fut));
85                body
86            }
87        };
88        Ok(http::Response::from_parts(res, body))
89    }
90
91    /// Convert [http::Response] into [Response].
92    pub fn from_http<T>(
93        hooks: &mut dyn WasiHttpHooks,
94        res: http::Response<T>,
95    ) -> (
96        Self,
97        impl Future<Output = Result<(), Error>> + Send + 'static,
98    )
99    where
100        T: http_body::Body<Data = Bytes> + Send + 'static,
101        T::Error: Into<Error>,
102    {
103        let (parts, body) = res.into_parts();
104        let (result_tx, result_rx) = tokio::sync::oneshot::channel();
105
106        let wasi_response = Response {
107            status: parts.status,
108            headers: FieldMap::new_immutable(hooks, parts.headers),
109            body: Body::Host {
110                body: body.map_err(Into::into).boxed_unsync(),
111                result_tx,
112            },
113        };
114
115        let io_future = async {
116            let Ok(fut) = result_rx.await else {
117                return Ok(());
118            };
119            Box::into_pin(fut).await
120        };
121
122        (wasi_response, io_future)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::default_hooks;
130    use core::future::Future;
131    use core::pin::pin;
132    use core::task::{Context, Poll, Waker};
133    use http_body_util::Full;
134
135    #[tokio::test]
136    async fn test_response_from_http() {
137        let http_response = http::Response::builder()
138            .status(StatusCode::OK)
139            .header("x-custom-header", "value123")
140            .body(Full::new(Bytes::from_static(b"hello wasm")))
141            .unwrap();
142
143        let (wasi_resp, io_future) = Response::from_http(default_hooks(), http_response);
144        assert_eq!(wasi_resp.status, StatusCode::OK);
145        assert_eq!(
146            wasi_resp.headers.get("x-custom-header").unwrap(),
147            "value123"
148        );
149        match wasi_resp.body {
150            Body::Host { body, result_tx } => {
151                let collected = body.collect().await;
152                assert!(collected.is_ok(), "Body stream failed unexpectedly");
153                let chunks = collected.unwrap().to_bytes();
154                assert_eq!(chunks, &b"hello wasm"[..]);
155                _ = result_tx.send(Box::new(async { Ok(()) }));
156            }
157            _ => panic!("Response body should be of type Host"),
158        }
159
160        let mut cx = Context::from_waker(Waker::noop());
161        let result = pin!(io_future).poll(&mut cx);
162        assert!(matches!(result, Poll::Ready(Ok(_))));
163    }
164}