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