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}