Skip to main content

spin_factor_outbound_http/
wasi_2023_10_18.rs

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