Skip to main content

wasmtime_wasi_http/p2/
http_impl.rs

1//! Implementation of the `wasi:http/outgoing-handler` interface.
2
3use crate::WasiHttpCtxView;
4use crate::p2::{
5    HttpResult,
6    bindings::http::{
7        outgoing_handler,
8        types::{self, Scheme},
9    },
10    error::internal_error,
11    http_request_error,
12    types::{HostFutureIncomingResponse, HostOutgoingRequest},
13};
14use bytes::Bytes;
15use http_body_util::{BodyExt, Empty};
16use hyper::Method;
17use std::pin::Pin;
18use wasmtime::component::Resource;
19
20impl outgoing_handler::Host for WasiHttpCtxView<'_> {
21    fn handle(
22        &mut self,
23        request_id: Resource<HostOutgoingRequest>,
24        options: Option<Resource<types::RequestOptions>>,
25    ) -> HttpResult<Resource<HostFutureIncomingResponse>> {
26        // `options` is an `own` parameter (`outgoing-handler.handle` takes ownership of it, per
27        // `wasi:http` types.wit), so it must be consumed from the table like `request` below.
28        let opts = match options {
29            Some(id) => Some(self.table.delete(id)?),
30            None => None,
31        };
32
33        let req = self.table.delete(request_id)?;
34        let mut builder = hyper::Request::builder();
35
36        builder = builder.method(match req.method {
37            types::Method::Get => Method::GET,
38            types::Method::Head => Method::HEAD,
39            types::Method::Post => Method::POST,
40            types::Method::Put => Method::PUT,
41            types::Method::Delete => Method::DELETE,
42            types::Method::Connect => Method::CONNECT,
43            types::Method::Options => Method::OPTIONS,
44            types::Method::Trace => Method::TRACE,
45            types::Method::Patch => Method::PATCH,
46            types::Method::Other(m) => match hyper::Method::from_bytes(m.as_bytes()) {
47                Ok(method) => method,
48                Err(_) => return Err(types::ErrorCode::HttpRequestMethodInvalid.into()),
49            },
50        });
51
52        let scheme = match req.scheme {
53            Some(scheme) => {
54                let scheme = match scheme {
55                    Scheme::Http => http::uri::Scheme::HTTP,
56                    Scheme::Https => http::uri::Scheme::HTTPS,
57                    Scheme::Other(scheme) => http::uri::Scheme::try_from(scheme.as_str())
58                        .map_err(|_| types::ErrorCode::HttpProtocolError)?,
59                };
60                if !self.hooks.is_supported_scheme(&scheme) {
61                    return Err(types::ErrorCode::HttpProtocolError.into());
62                }
63                scheme
64            }
65            // Note that a hook returning `None` here means that guests are
66            // required to specify a scheme themselves.
67            None => self
68                .hooks
69                .default_scheme()
70                .ok_or(types::ErrorCode::HttpProtocolError)?,
71        };
72
73        let authority = req.authority.unwrap_or_else(String::new);
74
75        let mut uri = http::Uri::builder()
76            .scheme(scheme)
77            .authority(authority.clone());
78
79        if let Some(path) = req.path_with_query {
80            uri = uri.path_and_query(path);
81        }
82
83        builder = builder.uri(uri.build().map_err(http_request_error)?);
84
85        if self.hooks.set_host_header() {
86            builder = builder.header(http::header::HOST, authority.as_str());
87        }
88
89        for (k, v) in req.headers.iter() {
90            builder = builder.header(k, v);
91        }
92
93        let body = req.body.unwrap_or_else(|| {
94            Empty::<Bytes>::new()
95                .map_err(|_| unreachable!("Infallible error"))
96                .boxed_unsync()
97        });
98        let body = body.map_err(Into::into).boxed_unsync();
99
100        let request = builder
101            .body(body)
102            .map_err(|err| internal_error(err.to_string()))?;
103
104        let future = self
105            .hooks
106            .send_request(request, opts, Box::new(async { Ok(()) }));
107        let future = wasmtime_wasi::runtime::spawn(async move {
108            let (res, io) = Pin::from(future).await?;
109            let io = wasmtime_wasi::runtime::spawn(async move {
110                match Pin::from(io).await {
111                    Ok(()) => {}
112                    // TODO: shouldn't throw away this error and ideally should
113                    // surface somewhere.
114                    Err(e) => tracing::warn!("dropping error {e}"),
115                }
116            });
117            let res = res.map(|b| b.boxed_unsync());
118            Ok((res, io))
119        });
120
121        Ok(self
122            .table
123            .push(HostFutureIncomingResponse::Pending(future))?)
124    }
125}
126
127mod named {
128    use crate::WasiHttpNamedView;
129    use crate::p2::bindings::http::types;
130    use crate::p2::bindings::named_imports::wasi::http::outgoing_handler;
131    use crate::p2::types::{HostFutureIncomingResponse, HostOutgoingRequest};
132    use crate::p2::{HttpError, HttpResult};
133    use wasmtime::component::Resource;
134    use wasmtime_wasi::{NamedId, WasiCtxNamedView};
135
136    impl<T> outgoing_handler::Host for WasiCtxNamedView<'_, T>
137    where
138        T: WasiHttpNamedView,
139    {
140        fn handle(
141            &mut self,
142            id: NamedId,
143            request_id: Resource<HostOutgoingRequest>,
144            options: Option<Resource<types::RequestOptions>>,
145        ) -> HttpResult<Resource<HostFutureIncomingResponse>> {
146            super::outgoing_handler::Host::handle(&mut self.0.http(id), request_id, options)
147        }
148
149        fn convert_error_code(&mut self, err: HttpError) -> wasmtime::Result<types::ErrorCode> {
150            err.downcast()
151        }
152    }
153}