wasmcloud_component/wrappers/
http.rs

1//! This module provides utilities for writing HTTP servers and clients using the WASI HTTP API.
2//!
3//! It's inspired by the WASI 0.3 proposal for <https://github.com/WebAssembly/wasi-http> and will
4//! be supported until the release of wasi:http@0.3.0. After that, this module will likely be deprecated.
5//!
6//! ```rust
7//! use wasmcloud_component::http;
8//!
9//! struct Component;
10//!
11//! http::export!(Component);
12//!
13//! // Implementing the [`Server`] trait for a component
14//! impl http::Server for Component {
15//!     fn handle(_request: http::IncomingRequest) -> http::Result<http::Response<impl http::OutgoingBody>> {
16//!         Ok(http::Response::new("Hello from Rust!"))
17//!     }
18//! }
19//! ```
20use core::fmt::Display;
21use core::marker::PhantomData;
22use core::ops::{Deref, DerefMut};
23
24use std::io::{Read, Write};
25
26use anyhow::{anyhow, Context as _};
27use wasi::http::types::{OutgoingResponse, ResponseOutparam};
28use wasi::io::streams::{InputStream, OutputStream, StreamError};
29
30pub use http::{
31    header, method, response, uri, HeaderMap, HeaderName, HeaderValue, Method, Request, Response,
32    StatusCode, Uri,
33};
34pub use wasi::http::types::ErrorCode;
35
36pub type Result<T, E = ErrorCode> = core::result::Result<T, E>;
37
38pub type IncomingRequest = Request<IncomingBody>;
39
40impl crate::From<Method> for wasi::http::types::Method {
41    fn from(method: Method) -> Self {
42        match method.as_str() {
43            "GET" => Self::Get,
44            "HEAD" => Self::Head,
45            "POST" => Self::Post,
46            "PUT" => Self::Put,
47            "DELETE" => Self::Delete,
48            "CONNECT" => Self::Connect,
49            "OPTIONS" => Self::Options,
50            "TRACE" => Self::Trace,
51            "PATCH" => Self::Patch,
52            _ => Self::Other(method.to_string()),
53        }
54    }
55}
56
57impl crate::TryFrom<wasi::http::types::Method> for Method {
58    type Error = method::InvalidMethod;
59
60    fn try_from(method: wasi::http::types::Method) -> Result<Self, Self::Error> {
61        match method {
62            wasi::http::types::Method::Get => Ok(Self::GET),
63            wasi::http::types::Method::Head => Ok(Self::HEAD),
64            wasi::http::types::Method::Post => Ok(Self::POST),
65            wasi::http::types::Method::Put => Ok(Self::PUT),
66            wasi::http::types::Method::Delete => Ok(Self::DELETE),
67            wasi::http::types::Method::Connect => Ok(Self::CONNECT),
68            wasi::http::types::Method::Options => Ok(Self::OPTIONS),
69            wasi::http::types::Method::Trace => Ok(Self::TRACE),
70            wasi::http::types::Method::Patch => Ok(Self::PATCH),
71            wasi::http::types::Method::Other(method) => method.parse(),
72        }
73    }
74}
75
76impl crate::From<uri::Scheme> for wasi::http::types::Scheme {
77    fn from(scheme: uri::Scheme) -> Self {
78        match scheme.as_str() {
79            "http" => Self::Http,
80            "https" => Self::Https,
81            _ => Self::Other(scheme.to_string()),
82        }
83    }
84}
85
86impl crate::TryFrom<wasi::http::types::Scheme> for http::uri::Scheme {
87    type Error = uri::InvalidUri;
88
89    fn try_from(scheme: wasi::http::types::Scheme) -> Result<Self, Self::Error> {
90        match scheme {
91            wasi::http::types::Scheme::Http => Ok(Self::HTTP),
92            wasi::http::types::Scheme::Https => Ok(Self::HTTPS),
93            wasi::http::types::Scheme::Other(scheme) => scheme.parse(),
94        }
95    }
96}
97
98#[derive(Debug)]
99pub enum FieldsToHeaderMapError {
100    InvalidHeaderName(header::InvalidHeaderName),
101    InvalidHeaderValue(header::InvalidHeaderValue),
102}
103
104impl Display for FieldsToHeaderMapError {
105    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106        match self {
107            FieldsToHeaderMapError::InvalidHeaderName(e) => write!(f, "invalid header name: {e}"),
108            FieldsToHeaderMapError::InvalidHeaderValue(e) => write!(f, "invalid header value: {e}"),
109        }
110    }
111}
112
113impl std::error::Error for FieldsToHeaderMapError {}
114
115impl crate::TryFrom<wasi::http::types::Fields> for HeaderMap {
116    type Error = FieldsToHeaderMapError;
117
118    fn try_from(fields: wasi::http::types::Fields) -> Result<Self, Self::Error> {
119        let mut headers = HeaderMap::new();
120        for (name, value) in fields.entries() {
121            let name =
122                HeaderName::try_from(name).map_err(FieldsToHeaderMapError::InvalidHeaderName)?;
123            let value =
124                HeaderValue::try_from(value).map_err(FieldsToHeaderMapError::InvalidHeaderValue)?;
125            match headers.entry(name) {
126                header::Entry::Vacant(entry) => {
127                    entry.insert(value);
128                }
129                header::Entry::Occupied(mut entry) => {
130                    entry.append(value);
131                }
132            };
133        }
134        Ok(headers)
135    }
136}
137
138impl crate::TryFrom<HeaderMap> for wasi::http::types::Fields {
139    type Error = wasi::http::types::HeaderError;
140
141    fn try_from(headers: HeaderMap) -> Result<Self, Self::Error> {
142        let fields = wasi::http::types::Fields::new();
143        for (name, value) in &headers {
144            fields.append(&name.to_string(), &value.as_bytes().to_vec())?;
145        }
146        Ok(fields)
147    }
148}
149
150/// Trait for implementing a type that can be written to an outgoing HTTP response body.
151///
152/// When implementing this trait, you should write your type to the provided `OutputStream` and then
153/// drop the stream. Finally, you should call `wasi::http::types::OutgoingBody::finish` to finish
154/// the body and return the result.
155///
156/// This trait is already implemented for common Rust types, and it's implemented for
157/// `wasi::http::types::IncomingBody` and `wasi::io::streams::InputStream` as well. This enables
158/// using any stream from a Wasm interface as an outgoing body.
159///
160/// ```ignore
161/// use std::io::Write;
162///
163/// impl wasmcloud_component::http::OutgoingBody for Vec<u8> {
164///    fn write(
165///        self,
166///        body: wasi::http::types::OutgoingBody,
167///        mut stream: wasi::io::streams::OutputStream,
168///    ) -> std::io::Result<()> {
169///        stream.write_all(&self)?;
170///        drop(stream);
171///        wasi::http::types::OutgoingBody::finish(body, None)
172///            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
173///     }
174/// }
175/// ```
176pub trait OutgoingBody {
177    fn write(
178        self,
179        body: wasi::http::types::OutgoingBody,
180        stream: OutputStream,
181    ) -> std::io::Result<()>;
182}
183
184impl OutgoingBody for () {
185    fn write(
186        self,
187        _body: wasi::http::types::OutgoingBody,
188        _stream: OutputStream,
189    ) -> std::io::Result<()> {
190        Ok(())
191    }
192}
193
194impl OutgoingBody for &[u8] {
195    fn write(
196        self,
197        body: wasi::http::types::OutgoingBody,
198        mut stream: OutputStream,
199    ) -> std::io::Result<()> {
200        stream.write_all(self)?;
201        drop(stream);
202        wasi::http::types::OutgoingBody::finish(body, None)
203            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
204    }
205}
206
207impl OutgoingBody for Box<[u8]> {
208    fn write(
209        self,
210        body: wasi::http::types::OutgoingBody,
211        stream: OutputStream,
212    ) -> std::io::Result<()> {
213        self.as_ref().write(body, stream)
214    }
215}
216
217impl OutgoingBody for Vec<u8> {
218    fn write(
219        self,
220        body: wasi::http::types::OutgoingBody,
221        stream: OutputStream,
222    ) -> std::io::Result<()> {
223        self.as_slice().write(body, stream)
224    }
225}
226
227impl OutgoingBody for &str {
228    fn write(
229        self,
230        body: wasi::http::types::OutgoingBody,
231        stream: OutputStream,
232    ) -> std::io::Result<()> {
233        self.as_bytes().write(body, stream)
234    }
235}
236
237impl OutgoingBody for Box<str> {
238    fn write(
239        self,
240        body: wasi::http::types::OutgoingBody,
241        stream: OutputStream,
242    ) -> std::io::Result<()> {
243        self.as_ref().write(body, stream)
244    }
245}
246
247impl OutgoingBody for String {
248    fn write(
249        self,
250        body: wasi::http::types::OutgoingBody,
251        stream: OutputStream,
252    ) -> std::io::Result<()> {
253        self.as_str().write(body, stream)
254    }
255}
256
257impl OutgoingBody for InputStream {
258    fn write(
259        self,
260        body: wasi::http::types::OutgoingBody,
261        stream: OutputStream,
262    ) -> std::io::Result<()> {
263        loop {
264            match stream.blocking_splice(&self, u64::MAX) {
265                Ok(0) | Err(StreamError::Closed) => break,
266                Ok(_) => continue,
267                Err(StreamError::LastOperationFailed(err)) => {
268                    return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
269                }
270            }
271        }
272        drop(stream);
273        wasi::http::types::OutgoingBody::finish(body, None)
274            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
275    }
276}
277
278impl OutgoingBody for wasi::http::types::IncomingBody {
279    fn write(
280        self,
281        body: wasi::http::types::OutgoingBody,
282        stream: OutputStream,
283    ) -> std::io::Result<()> {
284        let input = self
285            .stream()
286            .map_err(|()| std::io::Error::from(std::io::ErrorKind::Other))?;
287        loop {
288            match stream.blocking_splice(&input, u64::MAX) {
289                Ok(0) | Err(StreamError::Closed) => break,
290                Ok(_) => continue,
291                Err(StreamError::LastOperationFailed(err)) => {
292                    return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
293                }
294            }
295        }
296        drop(stream);
297        let _trailers = wasi::http::types::IncomingBody::finish(self);
298        // NOTE: getting trailers crashes Wasmtime 25, so avoid doing so
299        //let trailers = if let Some(trailers) = trailers.get() {
300        //    trailers
301        //} else {
302        //    trailers.subscribe().block();
303        //    trailers
304        //        .get()
305        //        .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::Other))?
306        //};
307        //let trailers = trailers.map_err(|()| std::io::Error::from(std::io::ErrorKind::Other))?;
308        //let trailers =
309        //    trailers.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
310        wasi::http::types::OutgoingBody::finish(body, None)
311            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
312    }
313}
314
315impl OutgoingBody for IncomingBody {
316    fn write(
317        self,
318        body: wasi::http::types::OutgoingBody,
319        stream: OutputStream,
320    ) -> std::io::Result<()> {
321        loop {
322            match stream.blocking_splice(&self.stream, u64::MAX) {
323                Ok(0) | Err(StreamError::Closed) => break,
324                Ok(_) => continue,
325                Err(StreamError::LastOperationFailed(err)) => {
326                    return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
327                }
328            }
329        }
330        drop(stream);
331        let trailers = self
332            .into_trailers_wasi()
333            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
334        wasi::http::types::OutgoingBody::finish(body, trailers)
335            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
336    }
337}
338
339/// Wraps a body, which implements [Read]
340#[derive(Clone, Copy, Debug)]
341pub struct ReadBody<T>(T);
342
343impl<T: Read> OutgoingBody for ReadBody<T> {
344    fn write(
345        mut self,
346        body: wasi::http::types::OutgoingBody,
347        mut stream: OutputStream,
348    ) -> std::io::Result<()> {
349        std::io::copy(&mut self.0, &mut stream)?;
350        drop(stream);
351        wasi::http::types::OutgoingBody::finish(body, None)
352            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
353    }
354}
355
356/// Wraps the incoming body of a request which just contains
357/// the stream and the body of the request itself. The bytes of the body
358/// are only read into memory explicitly. The implementation of [`OutgoingBody`]
359/// for this type will read the bytes from the stream and write them to the
360/// output stream.
361pub struct IncomingBody {
362    stream: InputStream,
363    body: wasi::http::types::IncomingBody,
364}
365
366impl TryFrom<wasi::http::types::IncomingBody> for IncomingBody {
367    type Error = anyhow::Error;
368
369    fn try_from(body: wasi::http::types::IncomingBody) -> Result<Self, Self::Error> {
370        let stream = body
371            .stream()
372            .map_err(|()| anyhow!("failed to get incoming request body"))?;
373        Ok(Self { body, stream })
374    }
375}
376
377impl TryFrom<wasi::http::types::IncomingRequest> for IncomingBody {
378    type Error = anyhow::Error;
379
380    fn try_from(request: wasi::http::types::IncomingRequest) -> Result<Self, Self::Error> {
381        let body = request
382            .consume()
383            .map_err(|()| anyhow!("failed to consume incoming request"))?;
384        body.try_into()
385    }
386}
387
388impl Deref for IncomingBody {
389    type Target = InputStream;
390
391    fn deref(&self) -> &Self::Target {
392        &self.stream
393    }
394}
395
396impl DerefMut for IncomingBody {
397    fn deref_mut(&mut self) -> &mut Self::Target {
398        &mut self.stream
399    }
400}
401
402impl Read for IncomingBody {
403    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
404        Read::read(&mut self.stream, buf)
405    }
406}
407
408impl IncomingBody {
409    pub fn into_trailers(self) -> anyhow::Result<Option<HeaderMap>> {
410        let trailers = self.into_trailers_wasi()?;
411        let trailers = trailers.map(crate::TryInto::try_into).transpose()?;
412        Ok(trailers)
413    }
414
415    pub fn into_trailers_wasi(self) -> anyhow::Result<Option<wasi::http::types::Fields>> {
416        let IncomingBody { body, stream } = self;
417        drop(stream);
418        let _trailers = wasi::http::types::IncomingBody::finish(body);
419        // NOTE: getting trailers crashes Wasmtime 25, so avoid doing so
420        //let trailers = if let Some(trailers) = trailers.get() {
421        //    trailers
422        //} else {
423        //    trailers.subscribe().block();
424        //    trailers.get().context("trailers missing")?
425        //};
426        //let trailers = trailers.map_err(|()| anyhow!("trailers already consumed"))?;
427        //trailers.context("failed to receive trailers")
428        Ok(None)
429    }
430}
431
432impl crate::TryFrom<wasi::http::types::IncomingRequest> for Request<IncomingBody> {
433    type Error = anyhow::Error;
434
435    fn try_from(request: wasi::http::types::IncomingRequest) -> Result<Self, Self::Error> {
436        let uri = Uri::builder();
437        let uri = if let Some(path_with_query) = request.path_with_query() {
438            uri.path_and_query(path_with_query)
439        } else {
440            uri.path_and_query("/")
441        };
442        let uri = if let Some(scheme) = request.scheme() {
443            let scheme = <uri::Scheme as crate::TryFrom<_>>::try_from(scheme)
444                .context("failed to convert scheme")?;
445            uri.scheme(scheme)
446        } else {
447            uri
448        };
449        let uri = if let Some(authority) = request.authority() {
450            uri.authority(authority)
451        } else {
452            uri
453        };
454        let uri = uri.build().context("failed to build URI")?;
455        let method = <Method as crate::TryFrom<_>>::try_from(request.method())
456            .context("failed to convert method")?;
457        let mut req = Request::builder().method(method).uri(uri);
458        let req_headers = req
459            .headers_mut()
460            .context("failed to construct header map")?;
461        *req_headers = crate::TryInto::try_into(request.headers())
462            .context("failed to convert header fields to header map")?;
463        let body = IncomingBody::try_from(request)?;
464        req.body(body).context("failed to construct request")
465    }
466}
467
468#[doc(hidden)]
469#[derive(Default, Debug, Copy, Clone)]
470pub struct IncomingHandler<T: ?Sized>(PhantomData<T>);
471
472pub enum ResponseError {
473    /// Status code is not valid
474    StatusCode(StatusCode),
475    /// Failed to set headers
476    Headers(wasi::http::types::HeaderError),
477    /// Failed to get outgoing response body
478    Body,
479    /// Failed to get outgoing response body stream
480    BodyStream,
481}
482
483/// Trait for implementing an HTTP server WebAssembly component that receives a
484/// [`IncomingRequest`] and returns a [`Response`].
485pub trait Server {
486    fn handle(request: IncomingRequest) -> Result<Response<impl OutgoingBody>, ErrorCode>;
487
488    fn request_error(err: anyhow::Error) {
489        eprintln!("failed to convert `wasi:http/types.incoming-request` to `http::Request`: {err}");
490    }
491
492    fn response_error(out: ResponseOutparam, err: ResponseError) {
493        match err {
494            ResponseError::StatusCode(code) => {
495                ResponseOutparam::set(
496                    out,
497                    Err(ErrorCode::InternalError(Some(format!(
498                        "code `{code}` is not a valid HTTP status code",
499                    )))),
500                );
501            }
502            ResponseError::Headers(err) => {
503                ResponseOutparam::set(
504                    out,
505                    Err(ErrorCode::InternalError(Some(format!(
506                        "{:#}",
507                        anyhow!(err).context("failed to set headers"),
508                    )))),
509                );
510            }
511            ResponseError::Body => {
512                ResponseOutparam::set(
513                    out,
514                    Err(ErrorCode::InternalError(Some(
515                        "failed to get response body".into(),
516                    ))),
517                );
518            }
519            ResponseError::BodyStream => {
520                ResponseOutparam::set(
521                    out,
522                    Err(ErrorCode::InternalError(Some(
523                        "failed to get response body stream".into(),
524                    ))),
525                );
526            }
527        }
528    }
529
530    fn body_error(err: std::io::Error) {
531        eprintln!("failed to write response body: {err}");
532    }
533}
534
535impl<T: Server + ?Sized> wasi::exports::http::incoming_handler::Guest for IncomingHandler<T> {
536    fn handle(
537        request: wasi::http::types::IncomingRequest,
538        response_out: wasi::http::types::ResponseOutparam,
539    ) {
540        match crate::TryInto::try_into(request) {
541            Ok(request) => match T::handle(request) {
542                Ok(response) => {
543                    let (
544                        response::Parts {
545                            status, headers, ..
546                        },
547                        body,
548                    ) = response.into_parts();
549
550                    let headers = match crate::TryInto::try_into(headers) {
551                        Ok(headers) => headers,
552                        Err(err) => {
553                            T::response_error(response_out, ResponseError::Headers(err));
554                            return;
555                        }
556                    };
557                    let resp_tx = OutgoingResponse::new(headers);
558                    if let Err(()) = resp_tx.set_status_code(status.as_u16()) {
559                        T::response_error(response_out, ResponseError::StatusCode(status));
560                        return;
561                    }
562
563                    let Ok(resp_body) = resp_tx.body() else {
564                        T::response_error(response_out, ResponseError::Body);
565                        return;
566                    };
567
568                    let Ok(stream) = resp_body.write() else {
569                        T::response_error(response_out, ResponseError::BodyStream);
570                        return;
571                    };
572
573                    ResponseOutparam::set(response_out, Ok(resp_tx));
574                    if let Err(err) = body.write(resp_body, stream) {
575                        T::body_error(err);
576                    }
577                }
578                Err(err) => ResponseOutparam::set(response_out, Err(err)),
579            },
580            Err(err) => T::request_error(err),
581        }
582    }
583}
584
585// Macro wrapper for wasi:http/incoming-handler
586
587/// Macro to export [`wasi::exports::http::incoming_handler::Guest`] implementation for a type that
588/// implements [`Server`]. This aims to be as similar as possible to [`wasi::http::proxy::export!`].
589#[macro_export]
590macro_rules! export {
591    ($t:ty) => {
592        type __IncomingHandlerExport = ::wasmcloud_component::http::IncomingHandler<$t>;
593        ::wasmcloud_component::wasi::http::proxy::export!(__IncomingHandlerExport with_types_in ::wasmcloud_component::wasi);
594    };
595}
596pub use export;