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}