Skip to main content

spin_wasip3_http/
lib.rs

1//! Experimental Rust SDK for WASIp3 HTTP.
2
3#![deny(missing_docs)]
4
5#[doc(hidden)]
6pub use wasip3;
7
8pub use wasip3::{
9    http_compat::{IncomingMessage, Request, Response},
10    wit_bindgen::{self, spawn},
11    wit_future, wit_stream,
12};
13
14use hyperium as http;
15use std::any::Any;
16use wasip3::{
17    http::types,
18    http_compat::{
19        http_from_wasi_request, http_from_wasi_response, http_into_wasi_request,
20        http_into_wasi_response,
21    },
22};
23
24/// A alias for [`std::result::Result`] that uses [`Error`] as the default error type.
25///
26/// This allows functions throughout the crate to return `Result<T>`
27/// instead of writing out `Result<T, Error>` explicitly.
28pub type Result<T, E = Error> = ::std::result::Result<T, E>;
29
30type HttpResult<T> = Result<T, types::ErrorCode>;
31
32/// The error type used for HTTP operations within the WASI environment.
33///
34/// This enum provides a unified representation of all errors that can occur
35/// during HTTP request or response handling, whether they originate from
36/// WASI-level error codes, dynamic runtime failures, or full HTTP responses
37/// returned as error results.
38///
39/// # See also
40/// - [`http::Error`]: Error type originating from the [`http`] crate.
41/// - [`wasip3::http::types::ErrorCode`]: Standard WASI HTTP error codes.
42/// - [`wasip3::http::types::Response`]: Used when an error represents an HTTP response body.
43#[derive(Debug)]
44pub enum Error {
45    /// A low-level WASI HTTP error code.
46    ///
47    /// Wraps [`wasip3::http::types::ErrorCode`] to represent
48    /// transport-level or protocol-level failures.
49    ErrorCode(wasip3::http::types::ErrorCode),
50    /// An error originating from the [`http`] crate.
51    ///
52    /// Covers errors encountered during the construction,
53    /// parsing, or validation of [`http`] types (e.g. invalid headers,
54    /// malformed URIs, or protocol violations).
55    HttpError(http::Error),
56    /// A dynamic application or library error.
57    ///
58    /// Used for any runtime error that implements [`std::error::Error`],
59    /// allowing flexibility for different error sources.
60    Other(Box<dyn std::error::Error + Send + Sync>),
61    /// An HTTP response treated as an error.
62    ///
63    /// Contains a full [`wasip3::http::types::Response`], such as
64    /// a `404 Not Found` or `500 Internal Server Error`, when
65    /// the response itself represents an application-level failure.
66    Response(wasip3::http::types::Response),
67}
68
69impl std::fmt::Display for Error {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            Error::ErrorCode(e) => write!(f, "{e}"),
73            Error::HttpError(e) => write!(f, "{e}"),
74            Error::Other(e) => write!(f, "{e}"),
75            Error::Response(resp) => match http::StatusCode::from_u16(resp.get_status_code()) {
76                Ok(status) => write!(f, "{status}"),
77                Err(_) => write!(f, "invalid status code {}", resp.get_status_code()),
78            },
79        }
80    }
81}
82
83impl std::error::Error for Error {}
84
85impl From<http::Error> for Error {
86    fn from(err: http::Error) -> Error {
87        Error::HttpError(err)
88    }
89}
90
91impl From<anyhow::Error> for Error {
92    fn from(err: anyhow::Error) -> Error {
93        match err.downcast::<types::ErrorCode>() {
94            Ok(code) => Error::ErrorCode(code),
95            Err(other) => match other.downcast::<Error>() {
96                Ok(err) => err,
97                Err(other) => Error::Other(other.into_boxed_dyn_error()),
98            },
99        }
100    }
101}
102
103impl From<std::convert::Infallible> for Error {
104    fn from(v: std::convert::Infallible) -> Self {
105        match v {}
106    }
107}
108
109impl From<types::ErrorCode> for Error {
110    fn from(code: types::ErrorCode) -> Self {
111        Error::ErrorCode(code)
112    }
113}
114
115impl From<types::Response> for Error {
116    fn from(resp: types::Response) -> Self {
117        Error::Response(resp)
118    }
119}
120
121impl<Ok: IntoResponse, Err: Into<Error>> IntoResponse for Result<Ok, Err> {
122    fn into_response(self) -> HttpResult<types::Response> {
123        match self {
124            Ok(ok) => ok.into_response(),
125            Err(err) => match err.into() {
126                Error::ErrorCode(code) => Err(code),
127                Error::Response(resp) => Ok(resp),
128                Error::HttpError(err) => match err {
129                    err if err.is::<http::method::InvalidMethod>() => {
130                        Err(types::ErrorCode::HttpRequestMethodInvalid)
131                    }
132                    err if err.is::<http::uri::InvalidUri>() => {
133                        Err(types::ErrorCode::HttpRequestUriInvalid)
134                    }
135                    err => Err(types::ErrorCode::InternalError(Some(err.to_string()))),
136                },
137                Error::Other(other) => {
138                    Err(types::ErrorCode::InternalError(Some(other.to_string())))
139                }
140            },
141        }
142    }
143}
144
145/// Sends an HTTP request and returns the corresponding [`wasip3::http::types::Response`].
146///
147/// This function converts the provided value into a [`wasip3::http::types::Request`] using the
148/// [`IntoRequest`] trait, dispatches it to the WASI HTTP handler, and awaits
149/// the resulting response. It provides a convenient high-level interface for
150/// issuing HTTP requests within a WASI environment.
151pub async fn send(request: impl IntoRequest) -> HttpResult<Response> {
152    let request = request.into_request()?;
153    let response = wasip3::http::client::send(request).await?;
154    Response::from_response(response)
155}
156
157/// A body type representing an empty payload.
158///
159/// This is a convenience alias for [`http_body_util::Empty<bytes::Bytes>`],
160/// used when constructing HTTP requests or responses with no body.
161///
162/// # Examples
163///
164/// ```ignore
165/// use spin_wasip3_http::EmptyBody;
166///
167/// let empty = EmptyBody::new();
168/// let response = http::Response::builder()
169///     .status(204)
170///     .body(empty)
171///     .unwrap();
172/// ```
173pub type EmptyBody = http_body_util::Empty<bytes::Bytes>;
174
175/// A body type representing a complete, in-memory payload.
176///
177/// This is a convenience alias for [`http_body_util::Full<T>`], used when the
178/// entire body is already available as a single value of type `T`.
179///
180/// It is typically used for sending small or pre-buffered request or response
181/// bodies without the need for streaming.
182///
183/// # Examples
184///
185/// ```ignore
186/// use spin_wasip3_http::FullBody;
187/// use bytes::Bytes;
188///
189/// let body = FullBody::new(Bytes::from("hello"));
190/// let request = http::Request::builder()
191///     .method("POST")
192///     .uri("https://example.com")
193///     .body(body)
194///     .unwrap();
195/// ```
196pub type FullBody<T> = http_body_util::Full<T>;
197
198/// A trait for constructing a value from a [`wasip3::http::types::Request`].
199///
200/// This is the inverse of [`IntoRequest`], allowing higher-level request
201/// types to be built from standardized WASI HTTP requests—for example,
202/// to parse structured payloads, extract query parameters, or perform
203/// request validation.
204///
205/// # See also
206/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`].
207pub trait FromRequest {
208    /// Attempts to construct `Self` from a [`wasip3::http::types::Request`].
209    fn from_request(req: wasip3::http::types::Request) -> HttpResult<Self>
210    where
211        Self: Sized;
212}
213
214impl FromRequest for types::Request {
215    fn from_request(req: types::Request) -> HttpResult<Self> {
216        Ok(req)
217    }
218}
219
220impl FromRequest for Request {
221    fn from_request(req: types::Request) -> HttpResult<Self> {
222        http_from_wasi_request(req)
223    }
224}
225
226/// A trait for any type that can be converted into a [`wasip3::http::types::Request`].
227///
228/// This trait provides a unified interface for adapting user-defined request
229/// types into the lower-level [`wasip3::http::types::Request`] format used by
230/// the WASI HTTP subsystem.  
231///
232/// Implementing `IntoRequest` allows custom builders or wrapper types to
233/// interoperate seamlessly with APIs that expect standardized WASI HTTP
234/// request objects.
235///
236/// # See also
237/// - [`FromRequest`]: The inverse conversion trait.
238pub trait IntoRequest {
239    /// Converts `self` into a [`wasip3::http::types::Request`].
240    fn into_request(self) -> HttpResult<wasip3::http::types::Request>;
241}
242
243impl<T> IntoRequest for http::Request<T>
244where
245    T: http_body::Body + Any,
246    T::Data: Into<Vec<u8>>,
247    T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
248{
249    fn into_request(self) -> HttpResult<types::Request> {
250        http_into_wasi_request(self)
251    }
252}
253
254/// A trait for constructing a value from a [`wasip3::http::types::Response`].
255///
256/// This is the inverse of [`IntoResponse`], allowing higher-level response
257/// types to be derived from standardized WASI HTTP responses—for example,
258/// to deserialize JSON payloads or map responses to domain-specific types.
259///
260/// # See also
261/// - [`IntoResponse`]: Converts a type into a [`wasip3::http::types::Response`].
262pub trait FromResponse {
263    /// Attempts to construct `Self` from a [`wasip3::http::types::Response`].
264    fn from_response(response: wasip3::http::types::Response) -> HttpResult<Self>
265    where
266        Self: Sized;
267}
268
269impl FromResponse for Response {
270    fn from_response(resp: types::Response) -> HttpResult<Self> {
271        http_from_wasi_response(resp)
272    }
273}
274
275/// A trait for any type that can be converted into a [`wasip3::http::types::Response`].
276///
277/// This trait provides a unified interface for adapting user-defined response
278/// types into the lower-level [`wasip3::http::types::Response`] format used by
279/// the WASI HTTP subsystem.  
280///
281/// Implementing `IntoResponse` enables ergonomic conversion from domain-level
282/// response types or builders into standardized WASI HTTP responses.
283///
284/// # See also
285/// - [`FromResponse`]: The inverse conversion trait.
286pub trait IntoResponse {
287    /// Converts `self` into a [`wasip3::http::types::Response`].
288    fn into_response(self) -> HttpResult<wasip3::http::types::Response>;
289}
290
291impl IntoResponse for types::Response {
292    fn into_response(self) -> HttpResult<types::Response> {
293        Ok(self)
294    }
295}
296
297impl<T> IntoResponse for (http::StatusCode, T)
298where
299    T: http_body::Body + Any,
300    T::Data: Into<Vec<u8>>,
301    T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
302{
303    fn into_response(self) -> HttpResult<types::Response> {
304        http_into_wasi_response(
305            http::Response::builder()
306                .status(self.0)
307                .body(self.1)
308                .unwrap(),
309        )
310    }
311}
312
313impl IntoResponse for &'static str {
314    fn into_response(self) -> HttpResult<types::Response> {
315        http::Response::new(http_body_util::Full::new(self.as_bytes())).into_response()
316    }
317}
318
319impl IntoResponse for String {
320    fn into_response(self) -> HttpResult<types::Response> {
321        http::Response::new(self).into_response()
322    }
323}
324
325impl<T> IntoResponse for http::Response<T>
326where
327    T: http_body::Body + Any,
328    T::Data: Into<Vec<u8>>,
329    T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
330{
331    fn into_response(self) -> HttpResult<types::Response> {
332        http_into_wasi_response(self)
333    }
334}
335
336/// Helpers for consuming an [`wasip3::http_compat::IncomingBody`].
337///
338/// This module provides extension traits and utilities for working with
339/// [`wasip3::http_compat::IncomingBody`] instances, such as streaming or collecting the entire
340/// body into memory.
341///
342/// These helpers make it easier to transform low-level streaming body types
343/// into higher-level forms (e.g., [`bytes::Bytes`]) for simplified data handling.
344pub mod body {
345    use bytes::Bytes;
346    use http_body_util::{BodyDataStream, BodyExt};
347    use wasip3::{
348        http::types::ErrorCode,
349        http_compat::{IncomingBody, IncomingMessage},
350    };
351
352    /// Extension trait providing convenient methods for consuming an [`IncomingBody`].
353    ///
354    /// This trait defines common patterns for handling HTTP body data in
355    /// asynchronous contexts. It allows converting the body into a stream
356    /// or fully collecting it into memory as a [`Bytes`] buffer.
357    #[allow(async_fn_in_trait)]
358    pub trait IncomingBodyExt {
359        /// Convert this [`IncomingBody`] into a [`BodyDataStream`].
360        ///
361        /// This method enables iteration over the body’s data chunks as they
362        /// arrive, without collecting them all into memory at once. It is
363        /// suitable for processing large or streaming payloads efficiently.
364        fn stream(self) -> BodyDataStream<Self>
365        where
366            Self: Sized;
367
368        /// Consume this [`IncomingBody`] and collect it into a single [`Bytes`] buffer.
369        ///
370        /// This method reads the entire body asynchronously and returns the
371        /// concatenated contents. It is best suited for small or bounded-size
372        /// payloads where holding all data in memory is acceptable.
373        async fn bytes(self) -> Result<Bytes, ErrorCode>;
374    }
375
376    impl<T: IncomingMessage> IncomingBodyExt for IncomingBody<T> {
377        /// Convert this [`IncomingBody`] into a [`BodyDataStream`].
378        fn stream(self) -> BodyDataStream<Self>
379        where
380            Self: Sized,
381        {
382            BodyDataStream::new(self)
383        }
384
385        /// Collect the [`IncomingBody`] into a single [`Bytes`] buffer.
386        async fn bytes(self) -> Result<Bytes, ErrorCode> {
387            self.collect().await.map(|c| c.to_bytes())
388        }
389    }
390}