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