spin_factor_outbound_http/
wasi_2023_10_18.rs

1use anyhow::Result;
2use wasmtime::component::{Linker, Resource};
3use wasmtime_wasi_http::bindings as latest;
4use wasmtime_wasi_http::{WasiHttpImpl, WasiHttpView};
5
6mod bindings {
7    use super::latest;
8
9    wasmtime::component::bindgen!({
10        path: "../../wit",
11        world: "wasi:http/proxy@0.2.0-rc-2023-10-18",
12        async: {
13            // Only need async exports
14            only_imports: [],
15        },
16        with: {
17            "wasi:io/poll/pollable": latest::io::poll::Pollable,
18            "wasi:io/streams/input-stream": latest::io::streams::InputStream,
19            "wasi:io/streams/output-stream": latest::io::streams::OutputStream,
20            "wasi:io/streams/error": latest::io::streams::Error,
21            "wasi:http/types/incoming-response": latest::http::types::IncomingResponse,
22            "wasi:http/types/incoming-request": latest::http::types::IncomingRequest,
23            "wasi:http/types/incoming-body": latest::http::types::IncomingBody,
24            "wasi:http/types/outgoing-response": latest::http::types::OutgoingResponse,
25            "wasi:http/types/outgoing-request": latest::http::types::OutgoingRequest,
26            "wasi:http/types/outgoing-body": latest::http::types::OutgoingBody,
27            "wasi:http/types/fields": latest::http::types::Fields,
28            "wasi:http/types/response-outparam": latest::http::types::ResponseOutparam,
29            "wasi:http/types/future-incoming-response": latest::http::types::FutureIncomingResponse,
30            "wasi:http/types/future-trailers": latest::http::types::FutureTrailers,
31        },
32        trappable_imports: true,
33    });
34}
35
36mod wasi {
37    pub use super::bindings::wasi::{http0_2_0_rc_2023_10_18 as http, io0_2_0_rc_2023_10_18 as io};
38}
39
40pub mod exports {
41    pub mod wasi {
42        pub use super::super::bindings::exports::wasi::http0_2_0_rc_2023_10_18 as http;
43    }
44}
45
46pub use bindings::{Proxy, ProxyIndices};
47use wasi::http::types::{
48    Error as HttpError, Fields, FutureIncomingResponse, FutureTrailers, Headers, IncomingBody,
49    IncomingRequest, IncomingResponse, Method, OutgoingBody, OutgoingRequest, OutgoingResponse,
50    RequestOptions, ResponseOutparam, Scheme, StatusCode, Trailers,
51};
52use wasi::io::poll::Pollable;
53use wasi::io::streams::{InputStream, OutputStream};
54
55use crate::wasi::WasiHttpImplInner;
56
57pub(crate) fn add_to_linker<T, F>(linker: &mut Linker<T>, closure: F) -> Result<()>
58where
59    T: Send,
60    F: Fn(&mut T) -> WasiHttpImpl<WasiHttpImplInner> + Send + Sync + Copy + 'static,
61{
62    wasi::http::types::add_to_linker_get_host(linker, closure)?;
63    wasi::http::outgoing_handler::add_to_linker_get_host(linker, closure)?;
64    Ok(())
65}
66
67impl<T> wasi::http::types::Host for WasiHttpImpl<T> where T: WasiHttpView + Send {}
68
69impl<T> wasi::http::types::HostFields for WasiHttpImpl<T>
70where
71    T: WasiHttpView + Send,
72{
73    fn new(
74        &mut self,
75        entries: Vec<(String, Vec<u8>)>,
76    ) -> wasmtime::Result<wasmtime::component::Resource<Fields>> {
77        match latest::http::types::HostFields::from_list(self, entries)? {
78            Ok(fields) => Ok(fields),
79            Err(e) => Err(e.into()),
80        }
81    }
82
83    fn get(
84        &mut self,
85        self_: wasmtime::component::Resource<Fields>,
86        name: String,
87    ) -> wasmtime::Result<Vec<Vec<u8>>> {
88        latest::http::types::HostFields::get(self, self_, name)
89    }
90
91    fn set(
92        &mut self,
93        self_: wasmtime::component::Resource<Fields>,
94        name: String,
95        value: Vec<Vec<u8>>,
96    ) -> wasmtime::Result<()> {
97        latest::http::types::HostFields::set(self, self_, name, value)??;
98        Ok(())
99    }
100
101    fn delete(
102        &mut self,
103        self_: wasmtime::component::Resource<Fields>,
104        name: String,
105    ) -> wasmtime::Result<()> {
106        latest::http::types::HostFields::delete(self, self_, name)??;
107        Ok(())
108    }
109
110    fn append(
111        &mut self,
112        self_: wasmtime::component::Resource<Fields>,
113        name: String,
114        value: Vec<u8>,
115    ) -> wasmtime::Result<()> {
116        latest::http::types::HostFields::append(self, self_, name, value)??;
117        Ok(())
118    }
119
120    fn entries(
121        &mut self,
122        self_: wasmtime::component::Resource<Fields>,
123    ) -> wasmtime::Result<Vec<(String, Vec<u8>)>> {
124        latest::http::types::HostFields::entries(self, self_)
125    }
126
127    fn clone(
128        &mut self,
129        self_: wasmtime::component::Resource<Fields>,
130    ) -> wasmtime::Result<wasmtime::component::Resource<Fields>> {
131        latest::http::types::HostFields::clone(self, self_)
132    }
133
134    fn drop(&mut self, rep: wasmtime::component::Resource<Fields>) -> wasmtime::Result<()> {
135        latest::http::types::HostFields::drop(self, rep)
136    }
137}
138
139impl<T> wasi::http::types::HostIncomingRequest for WasiHttpImpl<T>
140where
141    T: WasiHttpView + Send,
142{
143    fn method(
144        &mut self,
145        self_: wasmtime::component::Resource<IncomingRequest>,
146    ) -> wasmtime::Result<Method> {
147        latest::http::types::HostIncomingRequest::method(self, self_).map(|e| e.into())
148    }
149
150    fn path_with_query(
151        &mut self,
152        self_: wasmtime::component::Resource<IncomingRequest>,
153    ) -> wasmtime::Result<Option<String>> {
154        latest::http::types::HostIncomingRequest::path_with_query(self, self_)
155    }
156
157    fn scheme(
158        &mut self,
159        self_: wasmtime::component::Resource<IncomingRequest>,
160    ) -> wasmtime::Result<Option<Scheme>> {
161        latest::http::types::HostIncomingRequest::scheme(self, self_).map(|e| e.map(|e| e.into()))
162    }
163
164    fn authority(
165        &mut self,
166        self_: wasmtime::component::Resource<IncomingRequest>,
167    ) -> wasmtime::Result<Option<String>> {
168        latest::http::types::HostIncomingRequest::authority(self, self_)
169    }
170
171    fn headers(
172        &mut self,
173        self_: wasmtime::component::Resource<IncomingRequest>,
174    ) -> wasmtime::Result<wasmtime::component::Resource<Headers>> {
175        latest::http::types::HostIncomingRequest::headers(self, self_)
176    }
177
178    fn consume(
179        &mut self,
180        self_: wasmtime::component::Resource<IncomingRequest>,
181    ) -> wasmtime::Result<Result<wasmtime::component::Resource<IncomingBody>, ()>> {
182        latest::http::types::HostIncomingRequest::consume(self, self_)
183    }
184
185    fn drop(
186        &mut self,
187        rep: wasmtime::component::Resource<IncomingRequest>,
188    ) -> wasmtime::Result<()> {
189        latest::http::types::HostIncomingRequest::drop(self, rep)
190    }
191}
192
193impl<T> wasi::http::types::HostIncomingResponse for WasiHttpImpl<T>
194where
195    T: WasiHttpView + Send,
196{
197    fn status(
198        &mut self,
199        self_: wasmtime::component::Resource<IncomingResponse>,
200    ) -> wasmtime::Result<StatusCode> {
201        latest::http::types::HostIncomingResponse::status(self, self_)
202    }
203
204    fn headers(
205        &mut self,
206        self_: wasmtime::component::Resource<IncomingResponse>,
207    ) -> wasmtime::Result<wasmtime::component::Resource<Headers>> {
208        latest::http::types::HostIncomingResponse::headers(self, self_)
209    }
210
211    fn consume(
212        &mut self,
213        self_: wasmtime::component::Resource<IncomingResponse>,
214    ) -> wasmtime::Result<Result<wasmtime::component::Resource<IncomingBody>, ()>> {
215        latest::http::types::HostIncomingResponse::consume(self, self_)
216    }
217
218    fn drop(
219        &mut self,
220        rep: wasmtime::component::Resource<IncomingResponse>,
221    ) -> wasmtime::Result<()> {
222        latest::http::types::HostIncomingResponse::drop(self, rep)
223    }
224}
225
226impl<T> wasi::http::types::HostIncomingBody for WasiHttpImpl<T>
227where
228    T: WasiHttpView + Send,
229{
230    fn stream(
231        &mut self,
232        self_: wasmtime::component::Resource<IncomingBody>,
233    ) -> wasmtime::Result<Result<wasmtime::component::Resource<InputStream>, ()>> {
234        latest::http::types::HostIncomingBody::stream(self, self_)
235    }
236
237    fn finish(
238        &mut self,
239        this: wasmtime::component::Resource<IncomingBody>,
240    ) -> wasmtime::Result<wasmtime::component::Resource<FutureTrailers>> {
241        latest::http::types::HostIncomingBody::finish(self, this)
242    }
243
244    fn drop(&mut self, rep: wasmtime::component::Resource<IncomingBody>) -> wasmtime::Result<()> {
245        latest::http::types::HostIncomingBody::drop(self, rep)
246    }
247}
248
249impl<T> wasi::http::types::HostOutgoingRequest for WasiHttpImpl<T>
250where
251    T: WasiHttpView + Send,
252{
253    fn new(
254        &mut self,
255        method: Method,
256        path_with_query: Option<String>,
257        scheme: Option<Scheme>,
258        authority: Option<String>,
259        headers: wasmtime::component::Resource<Headers>,
260    ) -> wasmtime::Result<wasmtime::component::Resource<OutgoingRequest>> {
261        let headers = latest::http::types::HostFields::clone(self, headers)?;
262        let request = latest::http::types::HostOutgoingRequest::new(self, headers)?;
263        let borrow = || Resource::new_borrow(request.rep());
264
265        if let Err(()) =
266            latest::http::types::HostOutgoingRequest::set_method(self, borrow(), method.into())?
267        {
268            latest::http::types::HostOutgoingRequest::drop(self, request)?;
269            anyhow::bail!("invalid method supplied");
270        }
271
272        if let Err(()) = latest::http::types::HostOutgoingRequest::set_path_with_query(
273            self,
274            borrow(),
275            path_with_query,
276        )? {
277            latest::http::types::HostOutgoingRequest::drop(self, request)?;
278            anyhow::bail!("invalid path-with-query supplied");
279        }
280
281        // Historical WASI would fill in an empty authority with a port which
282        // got just enough working to get things through. Current WASI requires
283        // the authority, though, so perform the translation manually here.
284        let authority = authority.unwrap_or_else(|| match &scheme {
285            Some(Scheme::Http) | Some(Scheme::Other(_)) => ":80".to_string(),
286            Some(Scheme::Https) | None => ":443".to_string(),
287        });
288        if let Err(()) = latest::http::types::HostOutgoingRequest::set_scheme(
289            self,
290            borrow(),
291            scheme.map(|s| s.into()),
292        )? {
293            latest::http::types::HostOutgoingRequest::drop(self, request)?;
294            anyhow::bail!("invalid scheme supplied");
295        }
296
297        if let Err(()) = latest::http::types::HostOutgoingRequest::set_authority(
298            self,
299            borrow(),
300            Some(authority),
301        )? {
302            latest::http::types::HostOutgoingRequest::drop(self, request)?;
303            anyhow::bail!("invalid authority supplied");
304        }
305
306        Ok(request)
307    }
308
309    fn write(
310        &mut self,
311        self_: wasmtime::component::Resource<OutgoingRequest>,
312    ) -> wasmtime::Result<Result<wasmtime::component::Resource<OutgoingBody>, ()>> {
313        latest::http::types::HostOutgoingRequest::body(self, self_)
314    }
315
316    fn drop(
317        &mut self,
318        rep: wasmtime::component::Resource<OutgoingRequest>,
319    ) -> wasmtime::Result<()> {
320        latest::http::types::HostOutgoingRequest::drop(self, rep)
321    }
322}
323
324impl<T> wasi::http::types::HostOutgoingResponse for WasiHttpImpl<T>
325where
326    T: WasiHttpView + Send,
327{
328    fn new(
329        &mut self,
330        status_code: StatusCode,
331        headers: wasmtime::component::Resource<Headers>,
332    ) -> wasmtime::Result<wasmtime::component::Resource<OutgoingResponse>> {
333        let headers = latest::http::types::HostFields::clone(self, headers)?;
334        let response = latest::http::types::HostOutgoingResponse::new(self, headers)?;
335        let borrow = || Resource::new_borrow(response.rep());
336
337        if let Err(()) =
338            latest::http::types::HostOutgoingResponse::set_status_code(self, borrow(), status_code)?
339        {
340            latest::http::types::HostOutgoingResponse::drop(self, response)?;
341            anyhow::bail!("invalid status code supplied");
342        }
343
344        Ok(response)
345    }
346
347    fn write(
348        &mut self,
349        self_: wasmtime::component::Resource<OutgoingResponse>,
350    ) -> wasmtime::Result<Result<wasmtime::component::Resource<OutgoingBody>, ()>> {
351        latest::http::types::HostOutgoingResponse::body(self, self_)
352    }
353
354    fn drop(
355        &mut self,
356        rep: wasmtime::component::Resource<OutgoingResponse>,
357    ) -> wasmtime::Result<()> {
358        latest::http::types::HostOutgoingResponse::drop(self, rep)
359    }
360}
361
362impl<T> wasi::http::types::HostOutgoingBody for WasiHttpImpl<T>
363where
364    T: WasiHttpView + Send,
365{
366    fn write(
367        &mut self,
368        self_: wasmtime::component::Resource<OutgoingBody>,
369    ) -> wasmtime::Result<Result<wasmtime::component::Resource<OutputStream>, ()>> {
370        latest::http::types::HostOutgoingBody::write(self, self_)
371    }
372
373    fn finish(
374        &mut self,
375        this: wasmtime::component::Resource<OutgoingBody>,
376        trailers: Option<wasmtime::component::Resource<Trailers>>,
377    ) -> wasmtime::Result<()> {
378        latest::http::types::HostOutgoingBody::finish(self, this, trailers)?;
379        Ok(())
380    }
381
382    fn drop(&mut self, rep: wasmtime::component::Resource<OutgoingBody>) -> wasmtime::Result<()> {
383        latest::http::types::HostOutgoingBody::drop(self, rep)
384    }
385}
386
387impl<T> wasi::http::types::HostResponseOutparam for WasiHttpImpl<T>
388where
389    T: WasiHttpView + Send,
390{
391    fn set(
392        &mut self,
393        param: wasmtime::component::Resource<ResponseOutparam>,
394        response: Result<wasmtime::component::Resource<OutgoingResponse>, HttpError>,
395    ) -> wasmtime::Result<()> {
396        let response = response.map_err(|err| {
397            // TODO: probably need to figure out a better mapping between
398            // errors, but that seems like it would require string matching,
399            // which also seems not great.
400            let msg = match err {
401                HttpError::InvalidUrl(s) => format!("invalid url: {s}"),
402                HttpError::TimeoutError(s) => format!("timeout: {s}"),
403                HttpError::ProtocolError(s) => format!("protocol error: {s}"),
404                HttpError::UnexpectedError(s) => format!("unexpected error: {s}"),
405            };
406            latest::http::types::ErrorCode::InternalError(Some(msg))
407        });
408        latest::http::types::HostResponseOutparam::set(self, param, response)
409    }
410
411    fn drop(
412        &mut self,
413        rep: wasmtime::component::Resource<ResponseOutparam>,
414    ) -> wasmtime::Result<()> {
415        latest::http::types::HostResponseOutparam::drop(self, rep)
416    }
417}
418
419impl<T> wasi::http::types::HostFutureTrailers for WasiHttpImpl<T>
420where
421    T: WasiHttpView + Send,
422{
423    fn subscribe(
424        &mut self,
425        self_: wasmtime::component::Resource<FutureTrailers>,
426    ) -> wasmtime::Result<wasmtime::component::Resource<Pollable>> {
427        latest::http::types::HostFutureTrailers::subscribe(self, self_)
428    }
429
430    fn get(
431        &mut self,
432        self_: wasmtime::component::Resource<FutureTrailers>,
433    ) -> wasmtime::Result<Option<Result<wasmtime::component::Resource<Trailers>, HttpError>>> {
434        match latest::http::types::HostFutureTrailers::get(self, self_)? {
435            Some(Ok(Ok(Some(trailers)))) => Ok(Some(Ok(trailers))),
436            // Return an empty trailers if no trailers popped out since this
437            // version of WASI couldn't represent the lack of trailers.
438            Some(Ok(Ok(None))) => Ok(Some(Ok(latest::http::types::HostFields::new(self)?))),
439            Some(Ok(Err(e))) => Ok(Some(Err(e.into()))),
440            Some(Err(())) => Err(anyhow::anyhow!("trailers have already been retrieved")),
441            None => Ok(None),
442        }
443    }
444
445    fn drop(&mut self, rep: wasmtime::component::Resource<FutureTrailers>) -> wasmtime::Result<()> {
446        latest::http::types::HostFutureTrailers::drop(self, rep)
447    }
448}
449
450impl<T> wasi::http::types::HostFutureIncomingResponse for WasiHttpImpl<T>
451where
452    T: WasiHttpView + Send,
453{
454    fn get(
455        &mut self,
456        self_: wasmtime::component::Resource<FutureIncomingResponse>,
457    ) -> wasmtime::Result<
458        Option<Result<Result<wasmtime::component::Resource<IncomingResponse>, HttpError>, ()>>,
459    > {
460        match latest::http::types::HostFutureIncomingResponse::get(self, self_)? {
461            None => Ok(None),
462            Some(Ok(Ok(response))) => Ok(Some(Ok(Ok(response)))),
463            Some(Ok(Err(e))) => Ok(Some(Ok(Err(e.into())))),
464            Some(Err(())) => Ok(Some(Err(()))),
465        }
466    }
467
468    fn subscribe(
469        &mut self,
470        self_: wasmtime::component::Resource<FutureIncomingResponse>,
471    ) -> wasmtime::Result<wasmtime::component::Resource<Pollable>> {
472        latest::http::types::HostFutureIncomingResponse::subscribe(self, self_)
473    }
474
475    fn drop(
476        &mut self,
477        rep: wasmtime::component::Resource<FutureIncomingResponse>,
478    ) -> wasmtime::Result<()> {
479        latest::http::types::HostFutureIncomingResponse::drop(self, rep)
480    }
481}
482
483impl<T> wasi::http::outgoing_handler::Host for WasiHttpImpl<T>
484where
485    T: WasiHttpView + Send,
486{
487    fn handle(
488        &mut self,
489        request: wasmtime::component::Resource<OutgoingRequest>,
490        options: Option<RequestOptions>,
491    ) -> wasmtime::Result<Result<wasmtime::component::Resource<FutureIncomingResponse>, HttpError>>
492    {
493        let options = match options {
494            Some(RequestOptions {
495                connect_timeout_ms,
496                first_byte_timeout_ms,
497                between_bytes_timeout_ms,
498            }) => {
499                let options = latest::http::types::HostRequestOptions::new(self)?;
500                let borrow = || Resource::new_borrow(request.rep());
501
502                if let Some(ms) = connect_timeout_ms {
503                    if let Err(()) = latest::http::types::HostRequestOptions::set_connect_timeout(
504                        self,
505                        borrow(),
506                        Some(ms.into()),
507                    )? {
508                        latest::http::types::HostRequestOptions::drop(self, options)?;
509                        anyhow::bail!("invalid connect timeout supplied");
510                    }
511                }
512
513                if let Some(ms) = first_byte_timeout_ms {
514                    if let Err(()) =
515                        latest::http::types::HostRequestOptions::set_first_byte_timeout(
516                            self,
517                            borrow(),
518                            Some(ms.into()),
519                        )?
520                    {
521                        latest::http::types::HostRequestOptions::drop(self, options)?;
522                        anyhow::bail!("invalid first byte timeout supplied");
523                    }
524                }
525
526                if let Some(ms) = between_bytes_timeout_ms {
527                    if let Err(()) =
528                        latest::http::types::HostRequestOptions::set_between_bytes_timeout(
529                            self,
530                            borrow(),
531                            Some(ms.into()),
532                        )?
533                    {
534                        latest::http::types::HostRequestOptions::drop(self, options)?;
535                        anyhow::bail!("invalid between bytes timeout supplied");
536                    }
537                }
538
539                Some(options)
540            }
541            None => None,
542        };
543        match latest::http::outgoing_handler::Host::handle(self, request, options) {
544            Ok(resp) => Ok(Ok(resp)),
545            Err(e) => Ok(Err(e.downcast()?.into())),
546        }
547    }
548}
549
550macro_rules! convert {
551    () => {};
552    ($kind:ident $from:path [<=>] $to:path { $($body:tt)* } $($rest:tt)*) => {
553        convert!($kind $from => $to { $($body)* });
554        convert!($kind $to => $from { $($body)* });
555
556        convert!($($rest)*);
557    };
558    (struct $from:ty => $to:path { $($field:ident,)* } $($rest:tt)*) => {
559        impl From<$from> for $to {
560            fn from(e: $from) -> $to {
561                $to {
562                    $( $field: e.$field.into(), )*
563                }
564            }
565        }
566
567        convert!($($rest)*);
568    };
569    (enum $from:path => $to:path { $($variant:ident $(($e:ident))?,)* } $($rest:tt)*) => {
570        impl From<$from> for $to {
571            fn from(e: $from) -> $to {
572                use $from as A;
573                use $to as B;
574                match e {
575                    $(
576                        A::$variant $(($e))? => B::$variant $(($e.into()))?,
577                    )*
578                }
579            }
580        }
581
582        convert!($($rest)*);
583    };
584    (flags $from:path => $to:path { $($flag:ident,)* } $($rest:tt)*) => {
585        impl From<$from> for $to {
586            fn from(e: $from) -> $to {
587                use $from as A;
588                use $to as B;
589                let mut out = B::empty();
590                $(
591                    if e.contains(A::$flag) {
592                        out |= B::$flag;
593                    }
594                )*
595                out
596            }
597        }
598
599        convert!($($rest)*);
600    };
601}
602
603pub(crate) use convert;
604
605convert! {
606    enum latest::http::types::Method [<=>] Method {
607        Get,
608        Head,
609        Post,
610        Put,
611        Delete,
612        Connect,
613        Options,
614        Trace,
615        Patch,
616        Other(e),
617    }
618
619    enum latest::http::types::Scheme [<=>] Scheme {
620        Http,
621        Https,
622        Other(e),
623    }
624}
625
626impl From<latest::http::types::ErrorCode> for HttpError {
627    fn from(e: latest::http::types::ErrorCode) -> HttpError {
628        // TODO: should probably categorize this better given the typed info
629        // we have in `e`.
630        HttpError::UnexpectedError(e.to_string())
631    }
632}