Skip to main content

spin_sdk/
http.rs

1pub use wasip3::http_compat::{IncomingMessage, Request, Response};
2
3use hyperium as http;
4pub use hyperium::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
5use std::any::Any;
6use wasip3::{
7    http::types,
8    http_compat::{
9        http_from_wasi_request, http_from_wasi_response, http_into_wasi_request,
10        http_into_wasi_response,
11    },
12};
13
14pub mod body;
15/// gRPC helpers for serving tonic services.
16#[cfg(feature = "grpc")]
17#[cfg_attr(docsrs, doc(cfg(feature = "grpc")))]
18pub mod grpc;
19
20/// A alias for [`std::result::Result`] that uses [`Error`] as the default error type.
21///
22/// This allows functions throughout the crate to return `Result<T>`
23/// instead of writing out `Result<T, Error>` explicitly.
24pub type Result<T, E = Error> = ::std::result::Result<T, E>;
25
26type HttpResult<T> = Result<T, types::ErrorCode>;
27
28/// The error type used for HTTP operations within the WASI environment.
29///
30/// This enum provides a unified representation of all errors that can occur
31/// during HTTP request or response handling, whether they originate from
32/// WASI-level error codes, dynamic runtime failures, or full HTTP responses
33/// returned as error results.
34///
35/// # See also
36/// - [`http::Error`]: Error type originating from the [`http`] crate.
37/// - [`wasip3::http::types::ErrorCode`]: Standard WASI HTTP error codes.
38/// - [`wasip3::http::types::Response`]: Used when an error represents an HTTP response body.
39#[derive(Debug)]
40pub enum Error {
41    /// A low-level WASI HTTP error code.
42    ///
43    /// Wraps [`wasip3::http::types::ErrorCode`] to represent
44    /// transport-level or protocol-level failures.
45    ErrorCode(wasip3::http::types::ErrorCode),
46    /// An error originating from the [`http`] crate.
47    ///
48    /// Covers errors encountered during the construction,
49    /// parsing, or validation of [`http`] types (e.g. invalid headers,
50    /// malformed URIs, or protocol violations).
51    HttpError(http::Error),
52    /// A dynamic application or library error.
53    ///
54    /// Used for any runtime error that implements [`std::error::Error`],
55    /// allowing flexibility for different error sources.
56    Other(Box<dyn std::error::Error + Send + Sync>),
57    /// An HTTP response treated as an error.
58    ///
59    /// Contains a full [`wasip3::http::types::Response`], such as
60    /// a `404 Not Found` or `500 Internal Server Error`, when
61    /// the response itself represents an application-level failure.
62    Response(wasip3::http::types::Response),
63}
64
65impl std::fmt::Display for Error {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Error::ErrorCode(e) => write!(f, "{e}"),
69            Error::HttpError(e) => write!(f, "{e}"),
70            Error::Other(e) => write!(f, "{e}"),
71            Error::Response(resp) => match http::StatusCode::from_u16(resp.get_status_code()) {
72                Ok(status) => write!(f, "{status}"),
73                Err(_) => write!(f, "invalid status code {}", resp.get_status_code()),
74            },
75        }
76    }
77}
78
79impl std::error::Error for Error {}
80
81impl From<http::Error> for Error {
82    fn from(err: http::Error) -> Error {
83        Error::HttpError(err)
84    }
85}
86
87impl From<anyhow::Error> for Error {
88    fn from(err: anyhow::Error) -> Error {
89        match err.downcast::<types::ErrorCode>() {
90            Ok(code) => Error::ErrorCode(code),
91            Err(other) => match other.downcast::<Error>() {
92                Ok(err) => err,
93                Err(other) => Error::Other(other.into_boxed_dyn_error()),
94            },
95        }
96    }
97}
98
99impl From<std::convert::Infallible> for Error {
100    fn from(v: std::convert::Infallible) -> Self {
101        match v {}
102    }
103}
104
105impl From<types::ErrorCode> for Error {
106    fn from(code: types::ErrorCode) -> Self {
107        Error::ErrorCode(code)
108    }
109}
110
111impl From<types::Response> for Error {
112    fn from(resp: types::Response) -> Self {
113        Error::Response(resp)
114    }
115}
116
117impl From<String> for Error {
118    fn from(s: String) -> Self {
119        Error::other(s)
120    }
121}
122
123impl From<&'static str> for Error {
124    fn from(s: &'static str) -> Self {
125        Error::other(s)
126    }
127}
128
129impl Error {
130    /// Creates an [`Error::Other`] from a displayable message.
131    ///
132    /// This is a convenience constructor for producing errors
133    /// without manually wrapping in [`Error::Other`].
134    pub fn other(msg: impl Into<String>) -> Self {
135        anyhow::Error::msg(msg.into()).into()
136    }
137}
138
139impl<Ok: IntoResponse, Err: Into<Error>> IntoResponse for Result<Ok, Err> {
140    fn into_response(self) -> HttpResult<types::Response> {
141        match self {
142            Ok(ok) => ok.into_response(),
143            Err(err) => match err.into() {
144                Error::ErrorCode(code) => Err(code),
145                Error::Response(resp) => Ok(resp),
146                Error::HttpError(err) => match err {
147                    err if err.is::<http::method::InvalidMethod>() => {
148                        Err(types::ErrorCode::HttpRequestMethodInvalid)
149                    }
150                    err if err.is::<http::uri::InvalidUri>() => {
151                        Err(types::ErrorCode::HttpRequestUriInvalid)
152                    }
153                    err => Err(types::ErrorCode::InternalError(Some(err.to_string()))),
154                },
155                Error::Other(other) => {
156                    Err(types::ErrorCode::InternalError(Some(other.to_string())))
157                }
158            },
159        }
160    }
161}
162
163/// Sends an HTTP request and returns the corresponding [`wasip3::http::types::Response`].
164///
165/// This function converts the provided value into a [`wasip3::http::types::Request`] using the
166/// [`IntoRequest`] trait, dispatches it to the WASI HTTP handler, and awaits
167/// the resulting response. It provides a convenient high-level interface for
168/// issuing HTTP requests within a WASI environment.
169pub async fn send(request: impl IntoRequest) -> HttpResult<Response> {
170    let request = request.into_request()?;
171    let response = wasip3::http::client::send(request).await?;
172    Response::from_response(response)
173}
174
175/// Sends a GET request to the given URL.
176///
177/// This is a convenience wrapper around [`send`] that issues a GET request
178/// to the provided URL.
179///
180/// # Examples
181///
182/// ```ignore
183/// let resp = spin_sdk::http::get("https://example.com").await?;
184/// ```
185pub async fn get(url: impl AsRef<str>) -> HttpResult<Response> {
186    let request = http::Request::get(url.as_ref())
187        .body(EmptyBody::new())
188        .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
189    send(request).await
190}
191
192/// Sends a POST request with the given body.
193///
194/// The body can be any type that implements `Into<bytes::Bytes>`, such as
195/// `String`, `Vec<u8>`, `&'static str`, or `bytes::Bytes`.
196///
197/// # Examples
198///
199/// ```ignore
200/// let resp = spin_sdk::http::post("https://example.com/api", "hello").await?;
201/// ```
202pub async fn post(url: impl AsRef<str>, body: impl Into<bytes::Bytes>) -> HttpResult<Response> {
203    let request = http::Request::post(url.as_ref())
204        .body(FullBody::new(body.into()))
205        .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
206    send(request).await
207}
208
209/// Sends a PUT request with the given body.
210pub async fn put(url: impl AsRef<str>, body: impl Into<bytes::Bytes>) -> HttpResult<Response> {
211    let request = http::Request::put(url.as_ref())
212        .body(FullBody::new(body.into()))
213        .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
214    send(request).await
215}
216
217/// Sends a PATCH request with the given body.
218pub async fn patch(url: impl AsRef<str>, body: impl Into<bytes::Bytes>) -> HttpResult<Response> {
219    let request = http::Request::patch(url.as_ref())
220        .body(FullBody::new(body.into()))
221        .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
222    send(request).await
223}
224
225/// Sends a DELETE request to the given URL.
226pub async fn delete(url: impl AsRef<str>) -> HttpResult<Response> {
227    let request = http::Request::delete(url.as_ref())
228        .body(EmptyBody::new())
229        .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
230    send(request).await
231}
232
233/// A body type representing an empty payload.
234///
235/// This is a convenience alias for [`http_body_util::Empty<bytes::Bytes>`],
236/// used when constructing HTTP requests or responses with no body.
237///
238/// # Examples
239///
240/// ```ignore
241/// use spin_sdk::http::EmptyBody;
242///
243/// let empty = EmptyBody::new();
244/// let response = http::Response::builder()
245///     .status(204)
246///     .body(empty)
247///     .unwrap();
248/// ```
249pub type EmptyBody = http_body_util::Empty<bytes::Bytes>;
250
251/// A body type representing a complete, in-memory payload.
252///
253/// This is a convenience alias for [`http_body_util::Full<T>`], used when the
254/// entire body is already available as a single value of type `T`.
255///
256/// It is typically used for sending small or pre-buffered request or response
257/// bodies without the need for streaming.
258///
259/// # Examples
260///
261/// ```ignore
262/// use spin_sdk::http::FullBody;
263/// use bytes::Bytes;
264///
265/// let body = FullBody::new(Bytes::from("hello"));
266/// let request = http::Request::builder()
267///     .method("POST")
268///     .uri("https://example.com")
269///     .body(body)
270///     .unwrap();
271/// ```
272pub type FullBody<T> = http_body_util::Full<T>;
273
274/// A body type representing an optional payload.
275///
276/// This is a convenience alias for [`http_body_util::Either<FullBody<T>, EmptyBody>`],
277/// used when an HTTP request or response may or may not carry a body.
278///
279/// The `Left` variant holds a [`FullBody<T>`] for when a payload is present,
280/// while the `Right` variant holds an [`EmptyBody`] for when it is absent.
281///
282/// # Examples
283///
284/// ```ignore
285/// use spin_sdk::http::{OptionalBody, FullBody, EmptyBody};
286/// use bytes::Bytes;
287///
288/// // With a body
289/// let with_body: OptionalBody<Bytes> =
290///     http_body_util::Either::Left(FullBody::new(Bytes::from("hello")));
291///
292/// // Without a body
293/// let without_body: OptionalBody<Bytes> =
294///     http_body_util::Either::Right(EmptyBody::new());
295/// ```
296pub type OptionalBody<T> = http_body_util::Either<FullBody<T>, EmptyBody>;
297
298/// A trait for constructing a value from a [`wasip3::http::types::Request`].
299///
300/// This is the inverse of [`IntoRequest`], allowing higher-level request
301/// types to be built from standardized WASI HTTP requests—for example,
302/// to parse structured payloads, extract query parameters, or perform
303/// request validation.
304///
305/// # See also
306/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`].
307pub trait FromRequest {
308    /// Attempts to construct `Self` from a [`wasip3::http::types::Request`].
309    fn from_request(req: wasip3::http::types::Request) -> HttpResult<Self>
310    where
311        Self: Sized;
312}
313
314impl FromRequest for types::Request {
315    fn from_request(req: types::Request) -> HttpResult<Self> {
316        Ok(req)
317    }
318}
319
320impl FromRequest for Request {
321    fn from_request(req: types::Request) -> HttpResult<Self> {
322        http_from_wasi_request(req)
323    }
324}
325
326/// A trait for any type that can be converted into a [`wasip3::http::types::Request`].
327///
328/// This trait provides a unified interface for adapting user-defined request
329/// types into the lower-level [`wasip3::http::types::Request`] format used by
330/// the WASI HTTP subsystem.
331///
332/// Implementing `IntoRequest` allows custom builders or wrapper types to
333/// interoperate seamlessly with APIs that expect standardized WASI HTTP
334/// request objects.
335///
336/// # See also
337/// - [`FromRequest`]: The inverse conversion trait.
338pub trait IntoRequest {
339    /// Converts `self` into a [`wasip3::http::types::Request`].
340    fn into_request(self) -> HttpResult<wasip3::http::types::Request>;
341}
342
343impl<T> IntoRequest for http::Request<T>
344where
345    T: http_body::Body + Any,
346    T::Data: Into<Vec<u8>>,
347    T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
348{
349    fn into_request(self) -> HttpResult<types::Request> {
350        http_into_wasi_request(self)
351    }
352}
353
354/// A trait for constructing a value from a [`wasip3::http::types::Response`].
355///
356/// This is the inverse of [`IntoResponse`], allowing higher-level response
357/// types to be derived from standardized WASI HTTP responses—for example,
358/// to deserialize JSON payloads or map responses to domain-specific types.
359///
360/// # See also
361/// - [`IntoResponse`]: Converts a type into a [`wasip3::http::types::Response`].
362pub trait FromResponse {
363    /// Attempts to construct `Self` from a [`wasip3::http::types::Response`].
364    fn from_response(response: wasip3::http::types::Response) -> HttpResult<Self>
365    where
366        Self: Sized;
367}
368
369impl FromResponse for Response {
370    fn from_response(resp: types::Response) -> HttpResult<Self> {
371        http_from_wasi_response(resp)
372    }
373}
374
375/// A trait for any type that can be converted into a [`wasip3::http::types::Response`].
376///
377/// This trait provides a unified interface for adapting user-defined response
378/// types into the lower-level [`wasip3::http::types::Response`] format used by
379/// the WASI HTTP subsystem.
380///
381/// Implementing `IntoResponse` enables ergonomic conversion from domain-level
382/// response types or builders into standardized WASI HTTP responses.
383///
384/// # See also
385/// - [`FromResponse`]: The inverse conversion trait.
386pub trait IntoResponse {
387    /// Converts `self` into a [`wasip3::http::types::Response`].
388    fn into_response(self) -> HttpResult<wasip3::http::types::Response>;
389}
390
391impl IntoResponse for types::Response {
392    fn into_response(self) -> HttpResult<types::Response> {
393        Ok(self)
394    }
395}
396
397impl<T> IntoResponse for (http::StatusCode, T)
398where
399    T: http_body::Body + Any,
400    T::Data: Into<Vec<u8>>,
401    T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
402{
403    fn into_response(self) -> HttpResult<types::Response> {
404        http_into_wasi_response(
405            http::Response::builder()
406                .status(self.0)
407                .body(self.1)
408                .unwrap(),
409        )
410    }
411}
412
413impl IntoResponse for http::StatusCode {
414    fn into_response(self) -> HttpResult<types::Response> {
415        (self, EmptyBody::new()).into_response()
416    }
417}
418
419impl IntoResponse for &'static str {
420    fn into_response(self) -> HttpResult<types::Response> {
421        http::Response::new(http_body_util::Full::new(self.as_bytes())).into_response()
422    }
423}
424
425impl IntoResponse for String {
426    fn into_response(self) -> HttpResult<types::Response> {
427        http::Response::new(self).into_response()
428    }
429}
430
431impl<T> IntoResponse for http::Response<T>
432where
433    T: http_body::Body + Any,
434    T::Data: Into<Vec<u8>>,
435    T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
436{
437    fn into_response(self) -> HttpResult<types::Response> {
438        http_into_wasi_response(self)
439    }
440}
441
442impl IntoResponse for () {
443    fn into_response(self) -> HttpResult<types::Response> {
444        http::StatusCode::OK.into_response()
445    }
446}
447
448impl IntoResponse for &[u8] {
449    fn into_response(self) -> HttpResult<types::Response> {
450        self.to_vec().into_response()
451    }
452}
453
454impl IntoResponse for Vec<u8> {
455    fn into_response(self) -> HttpResult<types::Response> {
456        http::Response::new(FullBody::new(bytes::Bytes::from(self))).into_response()
457    }
458}
459
460impl IntoResponse for bytes::Bytes {
461    fn into_response(self) -> HttpResult<types::Response> {
462        http::Response::new(FullBody::new(self)).into_response()
463    }
464}
465
466impl<T> IntoResponse for (http::StatusCode, http::HeaderMap, T)
467where
468    T: http_body::Body + Any,
469    T::Data: Into<Vec<u8>>,
470    T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
471{
472    fn into_response(self) -> HttpResult<types::Response> {
473        let (status, headers, body) = self;
474        let mut resp = http::Response::builder().status(status).body(body).unwrap();
475        *resp.headers_mut() = headers;
476        resp.into_response()
477    }
478}
479
480/// A JSON wrapper for request and response bodies.
481///
482/// Wraps a value of type `T` and serializes it as JSON when used as a response,
483/// automatically setting the `Content-Type: application/json` header.
484///
485/// # Examples
486///
487/// ```ignore
488/// use spin_sdk::http::{Json, Request};
489/// use spin_sdk::http_service;
490/// use serde::Serialize;
491///
492/// #[derive(Serialize)]
493/// struct User { name: String }
494///
495/// #[http_service]
496/// async fn handler(_req: Request) -> impl IntoResponse {
497///     Json(User { name: "Alice".into() })
498/// }
499/// ```
500#[cfg(feature = "json")]
501#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
502pub struct Json<T>(pub T);
503
504#[cfg(feature = "json")]
505impl<T: serde::Serialize> IntoResponse for Json<T> {
506    fn into_response(self) -> HttpResult<types::Response> {
507        let body = serde_json::to_vec(&self.0)
508            .map_err(|e| types::ErrorCode::InternalError(Some(e.to_string())))?;
509        let mut resp = http::Response::builder()
510            .status(http::StatusCode::OK)
511            .body(FullBody::new(bytes::Bytes::from(body)))
512            .unwrap();
513        resp.headers_mut().insert(
514            http::header::CONTENT_TYPE,
515            http::HeaderValue::from_static("application/json"),
516        );
517        resp.into_response()
518    }
519}
520
521#[cfg(feature = "json")]
522impl<T: serde::Serialize> IntoResponse for (http::StatusCode, Json<T>) {
523    fn into_response(self) -> HttpResult<types::Response> {
524        let body = serde_json::to_vec(&self.1 .0)
525            .map_err(|e| types::ErrorCode::InternalError(Some(e.to_string())))?;
526        let mut resp = http::Response::builder()
527            .status(self.0)
528            .body(FullBody::new(bytes::Bytes::from(body)))
529            .unwrap();
530        resp.headers_mut().insert(
531            http::header::CONTENT_TYPE,
532            http::HeaderValue::from_static("application/json"),
533        );
534        resp.into_response()
535    }
536}