Skip to main content

spin_factor_outbound_http/
wasi_2023_10_18.rs

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