wasmtime_wasi_http/p3/host/
handler.rs1use crate::FieldMap;
2use crate::p3::bindings::http::client::{Host, HostWithStore};
3use crate::p3::bindings::http::types::{Request, Response};
4use crate::p3::body::{Body, BodyExt as _};
5use crate::p3::{HttpError, HttpResult};
6use crate::{Error, WasiHttp, WasiHttpCtxView};
7use core::task::{Context, Poll, Waker};
8use http_body_util::BodyExt as _;
9use std::sync::Arc;
10use tokio::sync::oneshot;
11use tokio::task::{self, JoinHandle};
12use tracing::debug;
13use wasmtime::AsContextMut as _;
14use wasmtime::component::{Accessor, HasData, Resource};
15use wasmtime::error::Context as _;
16
17struct AbortOnDropJoinHandle(JoinHandle<()>);
20
21impl Drop for AbortOnDropJoinHandle {
22 fn drop(&mut self) {
23 self.0.abort();
24 }
25}
26
27const DROPPED_FUTURE_ERROR: &str =
28 "Future indicating transmission result dropped without being resolved.";
29
30async fn io_task_result(rx: oneshot::Receiver<Result<(), Error>>) -> Result<(), Error> {
31 rx.await
32 .unwrap_or_else(|_| Err(Error::InternalError(Some(DROPPED_FUTURE_ERROR.to_string()))))
33}
34
35fn send_dummy_io_err<T, D>(
36 store: &Accessor<T, D>,
37 mut getter: impl FnMut(&mut T) -> WasiHttpCtxView<'_>,
38 e: Error,
39 io_result_tx: oneshot::Sender<Result<(), Error>>,
40) -> HttpError
41where
42 D: HasData,
43{
44 let err_code =
45 store.with(|mut store| getter(store.as_context_mut().data_mut()).error_to_p3(&e));
46 _ = io_result_tx.send(Err(e));
47 err_code.into()
48}
49
50impl<T> HostWithStore<T> for WasiHttp {
51 async fn send(
52 store: &Accessor<T, Self>,
53 req: Resource<Request>,
54 ) -> HttpResult<Resource<Response>> {
55 let getter = store.getter();
56 send(store, getter, req).await
57 }
58}
59
60async fn send<T, D>(
61 store: &Accessor<T, D>,
62 mut getter: impl FnMut(&mut T) -> WasiHttpCtxView<'_> + Copy + Unpin + Send + 'static,
63 req: Resource<Request>,
64) -> HttpResult<Resource<Response>>
65where
66 D: HasData,
67 T: 'static,
68{
69 let (io_result_tx, io_result_rx) = oneshot::channel();
71
72 let (res_result_tx, res_result_rx) = oneshot::channel();
74
75 let fut = store.with(|mut store| {
76 let WasiHttpCtxView { table, .. } = getter(store.data_mut());
77 let req = table
78 .delete(req)
79 .context("failed to delete request from table")
80 .map_err(HttpError::trap)?;
81 let (req, options) =
82 req.into_http_with_getter(&mut store, io_task_result(io_result_rx), getter)?;
83 HttpResult::Ok(getter(store.data_mut()).hooks.send_request(
84 req,
85 options.as_deref().copied(),
86 Box::new(async {
87 let Ok(fut) = res_result_rx.await else {
89 return Ok(());
90 };
91 Box::into_pin(fut).await
92 }),
93 ))
94 });
95 let fut = match fut {
96 Ok(fut) => fut,
97 Err(e) => match e.downcast() {
98 Ok(err_code) => {
99 _ = io_result_tx.send(Err(err_code.clone().into()));
100 return Err(err_code.into());
101 }
102 Err(e) => {
103 let e = Error::InternalError(Some(format!("{e}")));
104 return Err(send_dummy_io_err(store, getter, e, io_result_tx));
105 }
106 },
107 };
108 let (res, io) = match Box::into_pin(fut).await {
109 Ok(r) => r,
110 Err(e) => {
111 return Err(send_dummy_io_err(store, getter, e, io_result_tx));
112 }
113 };
114 let (
115 http::response::Parts {
116 status, headers, ..
117 },
118 body,
119 ) = res.into_parts();
120
121 let mut io = Box::into_pin(io);
122 let body = match io.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
123 Poll::Ready(Ok(())) => {
124 _ = io_result_tx.send(Ok(()));
125 body
126 }
127 Poll::Ready(Err(e)) => {
128 return Err(send_dummy_io_err(store, getter, e, io_result_tx));
129 }
130 Poll::Pending => {
131 let (done_tx, done_rx) = oneshot::channel::<()>();
133 let io = Arc::new(AbortOnDropJoinHandle(task::spawn(async move {
134 let _done_tx = done_tx;
135 let res = io.await;
136 debug!(?res, "`send_request` I/O future finished");
137 _ = io_result_tx.send(res);
138 })));
139 let store_io = Arc::clone(&io);
144 store
145 .spawn(async move |_| {
146 let _io = store_io;
147 _ = done_rx.await;
148 Ok(())
149 })
150 .map_err(HttpError::trap)?;
151 body.with_state(io).boxed_unsync()
154 }
155 };
156 store.with(|mut store| {
157 let view = getter(store.data_mut());
158 let res = Response {
159 status,
160 headers: FieldMap::new_immutable(view.hooks, headers),
161 body: Body::Host {
162 body,
163 result_tx: res_result_tx,
164 },
165 };
166 view.table
167 .push(res)
168 .context("failed to push response to table")
169 .map_err(HttpError::trap)
170 })
171}
172
173impl Host for WasiHttpCtxView<'_> {}
174
175mod named {
176 use crate::p3::bindings::http::types::{ErrorCode, Request, Response};
177 use crate::p3::bindings::named_imports::wasi::http::client::{Host, HostWithStore};
178 use crate::p3::{HttpError, HttpResult};
179 use crate::{WasiHttpNamed, WasiHttpNamedView};
180 use wasmtime::component::{Accessor, Resource};
181 use wasmtime_wasi::{NamedId, WasiCtxNamedView};
182
183 impl<T, U> HostWithStore<U> for WasiHttpNamed<T>
184 where
185 T: WasiHttpNamedView,
186 U: 'static,
187 {
188 async fn send(
189 store: &Accessor<U, Self>,
190 id: NamedId,
191 req: Resource<Request>,
192 ) -> HttpResult<Resource<Response>> {
193 let getter = store.getter();
194 super::send(store, move |data| getter(data).0.http(id), req).await
195 }
196 }
197
198 impl<T> Host for WasiCtxNamedView<'_, T>
199 where
200 T: WasiHttpNamedView,
201 {
202 fn convert_error_code(&mut self, error: HttpError) -> wasmtime::Result<ErrorCode> {
203 error.downcast()
204 }
205 }
206}