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