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#[cfg(feature = "http-middleware")]
176mod middleware {
177 use crate::wit_bindgen;
178
179 wit_bindgen::generate!({
180 runtime_path: "crate::wit_bindgen::rt",
181 world: "spin-sdk-middleware",
182 path: "wit",
183 with: {
184 "wasi:http/types@0.3.0": wasip3::http::types,
185 },
186 generate_all,
187 });
188}
189
190/// Sends an HTTP request to the next item in the middleware chain, and returns the [`wasip3::http::types::Response`]
191/// from that next item.
192///
193/// This function converts the provided value into a [`wasip3::http::types::Request`] using the
194/// [`IntoRequest`] trait, dispatches it to the WASI HTTP middleware handler, and awaits
195/// the resulting response.
196#[cfg(feature = "http-middleware")]
197pub async fn next(request: impl IntoRequest) -> HttpResult<Response> {
198 let request = request.into_request()?;
199 let response = middleware::wasi::http::handler::handle(request).await?;
200 Response::from_response(response)
201}
202
203/// Sends a GET request to the given URL.
204///
205/// This is a convenience wrapper around [`send`] that issues a GET request
206/// to the provided URL.
207///
208/// # Examples
209///
210/// ```ignore
211/// let resp = spin_sdk::http::get("https://example.com").await?;
212/// ```
213pub async fn get(url: impl AsRef<str>) -> HttpResult<Response> {
214 let request = http::Request::get(url.as_ref())
215 .body(EmptyBody::new())
216 .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
217 send(request).await
218}
219
220/// Sends a POST request with the given body.
221///
222/// The body can be any type that implements `Into<bytes::Bytes>`, such as
223/// `String`, `Vec<u8>`, `&'static str`, or `bytes::Bytes`.
224///
225/// # Examples
226///
227/// ```ignore
228/// let resp = spin_sdk::http::post("https://example.com/api", "hello").await?;
229/// ```
230pub async fn post(url: impl AsRef<str>, body: impl Into<bytes::Bytes>) -> HttpResult<Response> {
231 let request = http::Request::post(url.as_ref())
232 .body(FullBody::new(body.into()))
233 .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
234 send(request).await
235}
236
237/// Sends a PUT request with the given body.
238pub async fn put(url: impl AsRef<str>, body: impl Into<bytes::Bytes>) -> HttpResult<Response> {
239 let request = http::Request::put(url.as_ref())
240 .body(FullBody::new(body.into()))
241 .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
242 send(request).await
243}
244
245/// Sends a PATCH request with the given body.
246pub async fn patch(url: impl AsRef<str>, body: impl Into<bytes::Bytes>) -> HttpResult<Response> {
247 let request = http::Request::patch(url.as_ref())
248 .body(FullBody::new(body.into()))
249 .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
250 send(request).await
251}
252
253/// Sends a DELETE request to the given URL.
254pub async fn delete(url: impl AsRef<str>) -> HttpResult<Response> {
255 let request = http::Request::delete(url.as_ref())
256 .body(EmptyBody::new())
257 .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)?;
258 send(request).await
259}
260
261/// A body type representing an empty payload.
262///
263/// This is a convenience alias for [`http_body_util::Empty<bytes::Bytes>`],
264/// used when constructing HTTP requests or responses with no body.
265///
266/// # Examples
267///
268/// ```ignore
269/// use spin_sdk::http::EmptyBody;
270///
271/// let empty = EmptyBody::new();
272/// let response = http::Response::builder()
273/// .status(204)
274/// .body(empty)
275/// .unwrap();
276/// ```
277pub type EmptyBody = http_body_util::Empty<bytes::Bytes>;
278
279/// A body type representing a complete, in-memory payload.
280///
281/// This is a convenience alias for [`http_body_util::Full<T>`], used when the
282/// entire body is already available as a single value of type `T`.
283///
284/// It is typically used for sending small or pre-buffered request or response
285/// bodies without the need for streaming.
286///
287/// # Examples
288///
289/// ```ignore
290/// use spin_sdk::http::FullBody;
291/// use bytes::Bytes;
292///
293/// let body = FullBody::new(Bytes::from("hello"));
294/// let request = http::Request::builder()
295/// .method("POST")
296/// .uri("https://example.com")
297/// .body(body)
298/// .unwrap();
299/// ```
300pub type FullBody<T> = http_body_util::Full<T>;
301
302/// A body type representing an optional payload.
303///
304/// This is a convenience alias for [`http_body_util::Either<FullBody<T>, EmptyBody>`],
305/// used when an HTTP request or response may or may not carry a body.
306///
307/// The `Left` variant holds a [`FullBody<T>`] for when a payload is present,
308/// while the `Right` variant holds an [`EmptyBody`] for when it is absent.
309///
310/// # Examples
311///
312/// ```ignore
313/// use spin_sdk::http::{OptionalBody, FullBody, EmptyBody};
314/// use bytes::Bytes;
315///
316/// // With a body
317/// let with_body: OptionalBody<Bytes> =
318/// http_body_util::Either::Left(FullBody::new(Bytes::from("hello")));
319///
320/// // Without a body
321/// let without_body: OptionalBody<Bytes> =
322/// http_body_util::Either::Right(EmptyBody::new());
323/// ```
324pub type OptionalBody<T> = http_body_util::Either<FullBody<T>, EmptyBody>;
325
326/// A type-erased HTTP body.
327///
328/// This is a convenience alias for [`http_body_util::combinators::UnsyncBoxBody`]
329/// carrying [`bytes::Bytes`] data and [`anyhow::Error`] errors.
330///
331/// It is useful when a handler produces different concrete body types on
332/// different code paths, for example, a streaming body on one branch and an
333/// [`EmptyBody`] on another. Because those bodies have different types, they
334/// cannot be returned from the same `match` or `if` directly. Erasing each one
335/// to a `BoxBody` with [`box_body`] gives every branch the single, shared type
336/// [`Response<BoxBody>`](wasip3::http_compat::Response).
337///
338/// # Examples
339///
340/// ```no_run
341/// use spin_sdk::http::{box_body, BoxBody, EmptyBody, FullBody, Response};
342///
343/// # fn found() -> bool { true }
344/// let response: Response<BoxBody> = if found() {
345/// Response::new(box_body(FullBody::new(bytes::Bytes::from("hello"))))
346/// } else {
347/// Response::builder()
348/// .status(404)
349/// .body(box_body(EmptyBody::new()))
350/// .unwrap()
351/// };
352/// ```
353pub type BoxBody = http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, anyhow::Error>;
354
355/// Erase a concrete HTTP body into a [`BoxBody`].
356///
357/// This unifies several different body types behind a single
358/// [`Response<BoxBody>`](wasip3::http_compat::Response) type, which is useful for
359/// handlers whose branches produce different bodies (streaming, full, empty,
360/// and so on). Any body whose data is [`bytes::Bytes`] and whose error converts
361/// into [`anyhow::Error`], including the infallible [`EmptyBody`] and
362/// [`FullBody`], can be boxed.
363///
364/// See [`BoxBody`] for a complete example.
365pub fn box_body<B>(body: B) -> BoxBody
366where
367 B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
368 B::Error: Into<anyhow::Error>,
369{
370 use http_body_util::BodyExt;
371 body.map_err(Into::into).boxed_unsync()
372}
373
374/// A trait for constructing a value from a [`wasip3::http::types::Request`].
375///
376/// This is the inverse of [`IntoRequest`], allowing higher-level request
377/// types to be built from standardized WASI HTTP requests—for example,
378/// to parse structured payloads, extract query parameters, or perform
379/// request validation.
380///
381/// # See also
382/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`].
383pub trait FromRequest {
384 /// Attempts to construct `Self` from a [`wasip3::http::types::Request`].
385 fn from_request(req: wasip3::http::types::Request) -> HttpResult<Self>
386 where
387 Self: Sized;
388}
389
390impl FromRequest for types::Request {
391 fn from_request(req: types::Request) -> HttpResult<Self> {
392 Ok(req)
393 }
394}
395
396impl FromRequest for Request {
397 fn from_request(req: types::Request) -> HttpResult<Self> {
398 http_from_wasi_request(req)
399 }
400}
401
402/// A trait for any type that can be converted into a [`wasip3::http::types::Request`].
403///
404/// This trait provides a unified interface for adapting user-defined request
405/// types into the lower-level [`wasip3::http::types::Request`] format used by
406/// the WASI HTTP subsystem.
407///
408/// Implementing `IntoRequest` allows custom builders or wrapper types to
409/// interoperate seamlessly with APIs that expect standardized WASI HTTP
410/// request objects.
411///
412/// # See also
413/// - [`FromRequest`]: The inverse conversion trait.
414pub trait IntoRequest {
415 /// Converts `self` into a [`wasip3::http::types::Request`].
416 fn into_request(self) -> HttpResult<wasip3::http::types::Request>;
417}
418
419impl IntoRequest for wasip3::http::types::Request {
420 fn into_request(self) -> HttpResult<wasip3::http::types::Request> {
421 Ok(self)
422 }
423}
424
425impl<T> IntoRequest for http::Request<T>
426where
427 T: http_body::Body + Any,
428 T::Data: Into<Vec<u8>>,
429 T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
430{
431 fn into_request(self) -> HttpResult<types::Request> {
432 http_into_wasi_request(self)
433 }
434}
435
436/// A trait for constructing a value from a [`wasip3::http::types::Response`].
437///
438/// This is the inverse of [`IntoResponse`], allowing higher-level response
439/// types to be derived from standardized WASI HTTP responses—for example,
440/// to deserialize JSON payloads or map responses to domain-specific types.
441///
442/// # See also
443/// - [`IntoResponse`]: Converts a type into a [`wasip3::http::types::Response`].
444pub trait FromResponse {
445 /// Attempts to construct `Self` from a [`wasip3::http::types::Response`].
446 fn from_response(response: wasip3::http::types::Response) -> HttpResult<Self>
447 where
448 Self: Sized;
449}
450
451impl FromResponse for Response {
452 fn from_response(resp: types::Response) -> HttpResult<Self> {
453 http_from_wasi_response(resp)
454 }
455}
456
457/// A trait for any type that can be converted into a [`wasip3::http::types::Response`].
458///
459/// This trait provides a unified interface for adapting user-defined response
460/// types into the lower-level [`wasip3::http::types::Response`] format used by
461/// the WASI HTTP subsystem.
462///
463/// Implementing `IntoResponse` enables ergonomic conversion from domain-level
464/// response types or builders into standardized WASI HTTP responses.
465///
466/// # See also
467/// - [`FromResponse`]: The inverse conversion trait.
468pub trait IntoResponse {
469 /// Converts `self` into a [`wasip3::http::types::Response`].
470 fn into_response(self) -> HttpResult<wasip3::http::types::Response>;
471}
472
473impl IntoResponse for types::Response {
474 fn into_response(self) -> HttpResult<types::Response> {
475 Ok(self)
476 }
477}
478
479impl<T> IntoResponse for (http::StatusCode, T)
480where
481 T: http_body::Body + Any,
482 T::Data: Into<Vec<u8>>,
483 T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
484{
485 fn into_response(self) -> HttpResult<types::Response> {
486 http_into_wasi_response(
487 http::Response::builder()
488 .status(self.0)
489 .body(self.1)
490 .unwrap(),
491 )
492 }
493}
494
495impl IntoResponse for http::StatusCode {
496 fn into_response(self) -> HttpResult<types::Response> {
497 (self, EmptyBody::new()).into_response()
498 }
499}
500
501impl IntoResponse for &'static str {
502 fn into_response(self) -> HttpResult<types::Response> {
503 http::Response::new(http_body_util::Full::new(self.as_bytes())).into_response()
504 }
505}
506
507impl IntoResponse for String {
508 fn into_response(self) -> HttpResult<types::Response> {
509 http::Response::new(self).into_response()
510 }
511}
512
513impl<T> IntoResponse for http::Response<T>
514where
515 T: http_body::Body + Any,
516 T::Data: Into<Vec<u8>>,
517 T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
518{
519 fn into_response(self) -> HttpResult<types::Response> {
520 http_into_wasi_response(self)
521 }
522}
523
524impl IntoResponse for () {
525 fn into_response(self) -> HttpResult<types::Response> {
526 http::StatusCode::OK.into_response()
527 }
528}
529
530impl IntoResponse for &[u8] {
531 fn into_response(self) -> HttpResult<types::Response> {
532 self.to_vec().into_response()
533 }
534}
535
536impl IntoResponse for Vec<u8> {
537 fn into_response(self) -> HttpResult<types::Response> {
538 http::Response::new(FullBody::new(bytes::Bytes::from(self))).into_response()
539 }
540}
541
542impl IntoResponse for bytes::Bytes {
543 fn into_response(self) -> HttpResult<types::Response> {
544 http::Response::new(FullBody::new(self)).into_response()
545 }
546}
547
548impl<T> IntoResponse for (http::StatusCode, http::HeaderMap, T)
549where
550 T: http_body::Body + Any,
551 T::Data: Into<Vec<u8>>,
552 T::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
553{
554 fn into_response(self) -> HttpResult<types::Response> {
555 let (status, headers, body) = self;
556 let mut resp = http::Response::builder().status(status).body(body).unwrap();
557 *resp.headers_mut() = headers;
558 resp.into_response()
559 }
560}
561
562/// A JSON wrapper for request and response bodies.
563///
564/// Wraps a value of type `T` and serializes it as JSON when used as a response,
565/// automatically setting the `Content-Type: application/json` header.
566///
567/// # Examples
568///
569/// ```ignore
570/// use spin_sdk::http::{Json, Request};
571/// use spin_sdk::http_service;
572/// use serde::Serialize;
573///
574/// #[derive(Serialize)]
575/// struct User { name: String }
576///
577/// #[http_service]
578/// async fn handler(_req: Request) -> impl IntoResponse {
579/// Json(User { name: "Alice".into() })
580/// }
581/// ```
582#[cfg(feature = "json")]
583#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
584pub struct Json<T>(pub T);
585
586#[cfg(feature = "json")]
587impl<T: serde::Serialize> IntoResponse for Json<T> {
588 fn into_response(self) -> HttpResult<types::Response> {
589 let body = serde_json::to_vec(&self.0)
590 .map_err(|e| types::ErrorCode::InternalError(Some(e.to_string())))?;
591 let mut resp = http::Response::builder()
592 .status(http::StatusCode::OK)
593 .body(FullBody::new(bytes::Bytes::from(body)))
594 .unwrap();
595 resp.headers_mut().insert(
596 http::header::CONTENT_TYPE,
597 http::HeaderValue::from_static("application/json"),
598 );
599 resp.into_response()
600 }
601}
602
603#[cfg(feature = "json")]
604impl<T: serde::Serialize> IntoResponse for (http::StatusCode, Json<T>) {
605 fn into_response(self) -> HttpResult<types::Response> {
606 let body = serde_json::to_vec(&self.1.0)
607 .map_err(|e| types::ErrorCode::InternalError(Some(e.to_string())))?;
608 let mut resp = http::Response::builder()
609 .status(self.0)
610 .body(FullBody::new(bytes::Bytes::from(body)))
611 .unwrap();
612 resp.headers_mut().insert(
613 http::header::CONTENT_TYPE,
614 http::HeaderValue::from_static("application/json"),
615 );
616 resp.into_response()
617 }
618}