spin_sdk/http.rs
1//! Sending and receiving HTTP requests.
2//!
3//! This module includes:
4//!
5//! * Common HTTP types used in sending and receiving.
6//! * The [`send`] function for making HTTP requests.
7//! * The [`Router`] type for routing received HTTP requests.
8//! * The [`conversions`] and [`responses`] modules to make common cases easier.
9//!
10//! For writing a component which handles HTTP requests, see the
11//! [`http_component`](super::http_component) macro.
12
13/// Traits for converting between the various types
14pub mod conversions;
15
16use std::collections::HashMap;
17
18#[doc(inline)]
19pub use conversions::IntoResponse;
20#[doc(inline)]
21pub use types::{
22 ErrorCode, Headers, IncomingResponse, Method, OutgoingBody, OutgoingRequest, Scheme,
23 StatusCode, Trailers,
24};
25
26use self::conversions::{TryFromIncomingResponse, TryIntoOutgoingRequest};
27use super::wit::wasi::http0_2_0::types;
28use futures::SinkExt;
29use wasi::io::streams::{self, StreamError};
30
31/// Represents an incoming HTTP request.
32///
33/// If you don't need streaming access to the request body, you may find it
34/// easier to work with [Request] instead. To make outgoing requests, use
35/// [Request] (non-streaming) or [OutgoingRequest].
36///
37/// # Examples
38///
39/// Access the request body as a Rust stream:
40///
41/// ```no_run
42/// # use spin_sdk::http::{IncomingRequest, ResponseOutparam};
43/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) {
44/// use futures::stream::StreamExt;
45///
46/// let mut stream = req.into_body_stream();
47/// loop {
48/// let chunk = stream.next().await;
49/// match chunk {
50/// None => {
51/// println!("end of request body");
52/// break;
53/// }
54/// Some(Ok(chunk)) => {
55/// // process the data from the stream in a very realistic way
56/// println!("read {} bytes", chunk.len());
57/// }
58/// Some(Err(e)) => {
59/// println!("error reading body: {e:?}");
60/// break;
61/// }
62/// }
63/// }
64/// }
65/// ```
66///
67/// Access the body in a non-streaming way. This can be useful where your component
68/// must take IncomingRequest because some scenarios need streaming, but you
69/// have other scenarios that do not.
70///
71/// ```no_run
72/// # use spin_sdk::http::{IncomingRequest, ResponseOutparam};
73/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) {
74/// let body = req.into_body().await.unwrap();
75/// }
76/// ```
77#[doc(inline)]
78pub use types::IncomingRequest;
79
80/// Represents an outgoing HTTP response.
81///
82/// OutgoingResponse is used in conjunction with [ResponseOutparam] in cases where
83/// you want to stream the response body. In cases where you don't need to stream,
84/// it is often simpler to use [Response].
85///
86/// # Examples
87///
88/// Send a streaming response to an incoming request:
89///
90/// ```no_run
91/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse, ResponseOutparam};
92/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) {
93/// use futures::SinkExt;
94/// use std::io::Read;
95///
96/// let response_headers = Fields::from_list(&[
97/// ("content-type".to_owned(), "text/plain".into())
98/// ]).unwrap();
99///
100/// let response = OutgoingResponse::new(response_headers);
101/// response.set_status_code(200).unwrap();
102/// let mut response_body = response.take_body();
103///
104/// response_outparam.set(response);
105///
106/// let mut file = std::fs::File::open("war-and-peace.txt").unwrap();
107///
108/// loop {
109/// let mut buf = vec![0; 1024];
110/// let count = file.read(&mut buf).unwrap();
111///
112/// if count == 0 {
113/// break; // end of file
114/// }
115///
116/// buf.truncate(count);
117/// response_body.send(buf).await.unwrap();
118/// }
119/// }
120/// ```
121///
122/// Send a response then continue processing:
123///
124/// ```no_run
125/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse, ResponseOutparam};
126/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) {
127/// use futures::SinkExt;
128///
129/// let response_headers = Fields::from_list(&[
130/// ("content-type".to_owned(), "text/plain".into())
131/// ]).unwrap();
132///
133/// let response = OutgoingResponse::new(response_headers);
134/// response.set_status_code(200).unwrap();
135/// let mut response_body = response.take_body();
136///
137/// response_outparam.set(response);
138///
139/// response_body
140/// .send("Request accepted".as_bytes().to_vec())
141/// .await
142/// .unwrap();
143///
144/// // End the HTTP response so the client deems it complete.
145/// response_body.flush().await.unwrap();
146/// response_body.close().await.unwrap();
147/// drop(response_body);
148///
149/// // Perform any additional processing
150/// println!("While the cat's away, the mice will play");
151/// }
152/// ```
153#[doc(inline)]
154pub use types::OutgoingResponse;
155
156/// A common representation for headers and trailers.
157///
158/// # Examples
159///
160/// Initialise response headers from a list:
161///
162/// ```no_run
163/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse};
164/// # fn handle_request(req: IncomingRequest) {
165/// let response_headers = Fields::from_list(&[
166/// ("content-type".to_owned(), "text/plain".into())
167/// ]).unwrap();
168///
169/// let response = OutgoingResponse::new(response_headers);
170/// # }
171/// ```
172///
173/// Build response headers up dynamically:
174///
175/// ```no_run
176/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse};
177/// # fn handle_request(req: IncomingRequest) {
178/// let accepts_json = req.headers()
179/// .get(&"accept".to_owned())
180/// .iter()
181/// .flat_map(|v| String::from_utf8(v.clone()).ok())
182/// .any(|s| s == "application/json");
183///
184/// let response_headers = Fields::new();
185/// if accepts_json {
186/// response_headers.set(&"content-type".to_owned(), &["application/json".into()]).unwrap();
187/// };
188/// # }
189/// ```
190///
191/// # WASI resource documentation
192///
193#[doc(inline)]
194pub use types::Fields;
195
196/// A unified request object that can represent both incoming and outgoing requests.
197///
198/// This should be used in favor of [IncomingRequest] and [OutgoingRequest] when there
199/// is no need for streaming bodies.
200///
201/// # Examples
202///
203/// Read the method, a header, and the body an incoming HTTP request, without streaming:
204///
205/// ```no_run
206/// # use spin_sdk::http::{Method, Request, Response};
207///
208/// fn handle_request(req: Request) -> anyhow::Result<Response> {
209/// let method = req.method();
210/// let content_type = req.header("content-type");
211/// if *method == Method::Post {
212/// let body = String::from_utf8_lossy(req.body());
213/// }
214/// todo!()
215/// }
216/// ```
217///
218/// Send an outgoing GET request (no body) to `example.com`:
219///
220/// ```no_run
221/// use spin_sdk::http::{Request, Response};
222///
223/// # #[tokio::main]
224/// # async fn main() -> anyhow::Result<()> {
225/// let request = Request::get("https://example.com");
226/// let response: Response = spin_sdk::http::send(request).await?;
227/// # Ok(())
228/// # }
229/// ```
230///
231/// Send an outgoing POST request with a non-streaming body to `example.com`.
232///
233/// ```no_run
234/// use spin_sdk::http::{Request, Response};
235///
236/// # #[tokio::main]
237/// # async fn main() -> anyhow::Result<()> {
238/// let request = Request::post("https://example.com", "it's a-me, Spin")
239/// .header("content-type", "text/plain")
240/// .build();
241/// let response: Response = spin_sdk::http::send(request).await?;
242/// # Ok(())
243/// # }
244/// ```
245///
246/// Build and send an outgoing request without using the helper shortcut.
247///
248/// ```no_run
249/// use spin_sdk::http::{Method, Request, Response};
250///
251/// # #[tokio::main]
252/// # async fn main() -> anyhow::Result<()> {
253/// let mut request = Request::new(Method::Put, "https://example.com/message/safety");
254/// request.set_header("content-type", "text/plain");
255/// *request.body_mut() = "beware the crocodile".as_bytes().to_vec();
256/// let response: Response = spin_sdk::http::send(request).await?;
257/// # Ok(())
258/// # }
259/// ```
260///
261/// Build and send an outgoing request using the fluent builder.
262///
263/// ```no_run
264/// use spin_sdk::http::{Method, Request, Response};
265///
266/// # #[tokio::main]
267/// # async fn main() -> anyhow::Result<()> {
268/// let request = Request::builder()
269/// .uri("https://example.com/message/motivational")
270/// .method(Method::Put)
271/// .header("content-type", "text/plain")
272/// .body("the capybaras of creativity fly higher than the bluebirds of banality")
273/// .build();
274/// let response: Response = spin_sdk::http::send(request).await?;
275/// # Ok(())
276/// # }
277/// ```
278pub struct Request {
279 /// The method of the request
280 method: Method,
281 /// The uri for the request
282 ///
283 /// The first item is set to `None` if the supplied uri is malformed
284 uri: (Option<hyperium::Uri>, String),
285 /// The request headers
286 headers: HashMap<String, HeaderValue>,
287 /// The request body as bytes
288 body: Vec<u8>,
289}
290
291impl Request {
292 /// Creates a new request from a method and uri
293 pub fn new(method: Method, uri: impl Into<String>) -> Self {
294 Self {
295 method,
296 uri: Self::parse_uri(uri.into()),
297 headers: HashMap::new(),
298 body: Vec::new(),
299 }
300 }
301
302 /// Creates a [`RequestBuilder`]
303 pub fn builder() -> RequestBuilder {
304 RequestBuilder::new(Method::Get, "/")
305 }
306
307 /// Creates a [`RequestBuilder`] to GET the given `uri`
308 pub fn get(uri: impl Into<String>) -> RequestBuilder {
309 RequestBuilder::new(Method::Get, uri)
310 }
311
312 /// Creates a [`RequestBuilder`] to POST the given `body` to `uri`
313 pub fn post(uri: impl Into<String>, body: impl conversions::IntoBody) -> RequestBuilder {
314 let mut builder = RequestBuilder::new(Method::Post, uri);
315 builder.body(body);
316 builder
317 }
318
319 /// Creates a [`RequestBuilder`] to PUT the given `body` to `uri`
320 pub fn put(uri: impl Into<String>, body: impl conversions::IntoBody) -> RequestBuilder {
321 let mut builder = RequestBuilder::new(Method::Put, uri);
322 builder.body(body);
323 builder
324 }
325
326 /// Creates a [`RequestBuilder`] to PATCH the resource specified by `uri`
327 pub fn patch(uri: impl Into<String>, body: impl conversions::IntoBody) -> RequestBuilder {
328 let mut builder = RequestBuilder::new(Method::Patch, uri);
329 builder.body(body);
330 builder
331 }
332
333 /// Creates a [`RequestBuilder`] to DELETE the resource specified by `uri`
334 pub fn delete(uri: impl Into<String>) -> RequestBuilder {
335 RequestBuilder::new(Method::Delete, uri)
336 }
337
338 /// The request method
339 pub fn method(&self) -> &Method {
340 &self.method
341 }
342
343 /// The request uri
344 pub fn uri(&self) -> &str {
345 &self.uri.1
346 }
347
348 /// The request uri path
349 pub fn path(&self) -> &str {
350 self.uri.0.as_ref().map(|u| u.path()).unwrap_or_default()
351 }
352
353 /// The request uri query
354 pub fn query(&self) -> &str {
355 self.uri
356 .0
357 .as_ref()
358 .and_then(|u| u.query())
359 .unwrap_or_default()
360 }
361
362 /// The request headers
363 pub fn headers(&self) -> impl Iterator<Item = (&str, &HeaderValue)> {
364 self.headers.iter().map(|(k, v)| (k.as_str(), v))
365 }
366
367 /// Return a header value
368 ///
369 /// Will return `None` if the header does not exist.
370 pub fn header(&self, name: &str) -> Option<&HeaderValue> {
371 self.headers.get(&name.to_lowercase())
372 }
373
374 /// Set a header
375 pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
376 self.headers.insert(
377 name.into(),
378 HeaderValue {
379 inner: HeaderValueRep::String(value.into()),
380 },
381 );
382 }
383
384 /// The request body
385 pub fn body(&self) -> &[u8] {
386 &self.body
387 }
388
389 /// The request body
390 pub fn body_mut(&mut self) -> &mut Vec<u8> {
391 &mut self.body
392 }
393
394 /// Consume this type and return its body
395 pub fn into_body(self) -> Vec<u8> {
396 self.body
397 }
398
399 fn parse_uri(uri: String) -> (Option<hyperium::Uri>, String) {
400 (
401 hyperium::Uri::try_from(&uri)
402 .or_else(|_| hyperium::Uri::try_from(&format!("http://{uri}")))
403 .ok(),
404 uri,
405 )
406 }
407
408 /// Whether the request is an HTTPS request
409 fn is_https(&self) -> bool {
410 self.uri
411 .0
412 .as_ref()
413 .and_then(|u| u.scheme())
414 .map(|s| s == &hyperium::uri::Scheme::HTTPS)
415 .unwrap_or(true)
416 }
417
418 /// The URI's authority
419 fn authority(&self) -> Option<&str> {
420 self.uri
421 .0
422 .as_ref()
423 .and_then(|u| u.authority())
424 .map(|a| a.as_str())
425 }
426
427 /// The request path and query combined
428 pub fn path_and_query(&self) -> Option<&str> {
429 self.uri
430 .0
431 .as_ref()
432 .and_then(|u| u.path_and_query())
433 .map(|s| s.as_str())
434 }
435}
436
437/// A builder for non-streaming outgoing HTTP requests. You can obtain
438/// a RequestBuilder from the [Request::builder()] method, or from
439/// method-specific helpers such as [Request::get()] or [Request::post()].
440///
441/// # Examples
442///
443/// Use a method helper to build an outgoing POST request:
444///
445/// ```no_run
446/// use spin_sdk::http::{Request, Response};
447///
448/// # #[tokio::main]
449/// # async fn main() -> anyhow::Result<()> {
450/// let request = Request::post("https://example.com", "it's a-me, Spin")
451/// .header("content-type", "text/plain")
452/// .build();
453/// let response: Response = spin_sdk::http::send(request).await?;
454/// # Ok(())
455/// # }
456/// ```
457///
458/// Build and send an outgoing request using the RequestBuilder.
459///
460/// ```no_run
461/// use spin_sdk::http::{Method, Request, Response};
462///
463/// # #[tokio::main]
464/// # async fn main() -> anyhow::Result<()> {
465/// let request = Request::builder()
466/// .uri("https://example.com/message/motivational")
467/// .method(Method::Put)
468/// .header("content-type", "text/plain")
469/// .body("the capybaras of creativity fly higher than the bluebirds of banality")
470/// .build();
471/// let response: Response = spin_sdk::http::send(request).await?;
472/// # Ok(())
473/// # }
474/// ```
475pub struct RequestBuilder {
476 request: Request,
477}
478
479impl RequestBuilder {
480 /// Create a new `RequestBuilder`
481 pub fn new(method: Method, uri: impl Into<String>) -> Self {
482 Self {
483 request: Request::new(method, uri.into()),
484 }
485 }
486
487 /// Set the method
488 pub fn method(&mut self, method: Method) -> &mut Self {
489 self.request.method = method;
490 self
491 }
492
493 /// Set the uri
494 pub fn uri(&mut self, uri: impl Into<String>) -> &mut Self {
495 self.request.uri = Request::parse_uri(uri.into());
496 self
497 }
498
499 /// Set the headers
500 pub fn headers(&mut self, headers: impl conversions::IntoHeaders) -> &mut Self {
501 self.request.headers = into_header_rep(headers);
502 self
503 }
504
505 /// Set a header
506 pub fn header(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
507 self.request
508 .headers
509 .insert(key.into().to_lowercase(), HeaderValue::string(value.into()));
510 self
511 }
512
513 /// Set the body
514 pub fn body(&mut self, body: impl conversions::IntoBody) -> &mut Self {
515 self.request.body = body.into_body();
516 self
517 }
518
519 /// Build the `Request`
520 pub fn build(&mut self) -> Request {
521 std::mem::replace(&mut self.request, Request::new(Method::Get, "/"))
522 }
523}
524
525/// A unified response object that can represent both outgoing and incoming responses.
526///
527/// This should be used in favor of `OutgoingResponse` and `IncomingResponse` when there
528/// is no need for streaming bodies.
529///
530/// # Examples
531///
532/// Send a response to an incoming HTTP request:
533///
534/// ```no_run
535/// use spin_sdk::http::{Request, Response};
536///
537/// fn handle_request(req: Request) -> anyhow::Result<Response> {
538/// Ok(Response::builder()
539/// .status(200)
540/// .header("content-type", "text/plain")
541/// .body("Hello, world")
542/// .build())
543/// }
544/// ```
545///
546/// Parse a response from an outgoing HTTP request:
547///
548/// ```no_run
549/// # use spin_sdk::http::{Request, Response};
550/// #[derive(serde::Deserialize)]
551/// struct User {
552/// name: String,
553/// }
554///
555/// # #[tokio::main]
556/// # async fn main() -> anyhow::Result<()> {
557/// let request = Request::get("https://example.com");
558/// let response: Response = spin_sdk::http::send(request).await?;
559/// if *response.status() == 200 {
560/// let body = response.body();
561/// let user: User = serde_json::from_slice(body)?;
562/// }
563/// # Ok(())
564/// # }
565/// ```
566pub struct Response {
567 /// The status of the response
568 status: StatusCode,
569 /// The response headers
570 headers: HashMap<String, HeaderValue>,
571 /// The body of the response as bytes
572 body: Vec<u8>,
573}
574
575impl Response {
576 /// Create a new response from a status and body
577 pub fn new(status: impl conversions::IntoStatusCode, body: impl conversions::IntoBody) -> Self {
578 Self {
579 status: status.into_status_code(),
580 headers: HashMap::new(),
581 body: body.into_body(),
582 }
583 }
584
585 /// The response status
586 pub fn status(&self) -> &StatusCode {
587 &self.status
588 }
589
590 /// The request headers
591 pub fn headers(&self) -> impl Iterator<Item = (&str, &HeaderValue)> {
592 self.headers.iter().map(|(k, v)| (k.as_str(), v))
593 }
594
595 /// Return a header value
596 ///
597 /// Will return `None` if the header does not exist.
598 pub fn header(&self, name: &str) -> Option<&HeaderValue> {
599 self.headers.get(&name.to_lowercase())
600 }
601
602 /// Set a response header
603 pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
604 self.headers.insert(
605 name.into(),
606 HeaderValue {
607 inner: HeaderValueRep::String(value.into()),
608 },
609 );
610 }
611
612 /// The response body
613 pub fn body(&self) -> &[u8] {
614 &self.body
615 }
616
617 /// The response body
618 pub fn body_mut(&mut self) -> &mut Vec<u8> {
619 &mut self.body
620 }
621
622 /// Consume this type and return its body
623 pub fn into_body(self) -> Vec<u8> {
624 self.body
625 }
626
627 /// Converts this response into a [`ResponseBuilder`]. This can be used to
628 /// update a response before passing it on.
629 pub fn into_builder(self) -> ResponseBuilder {
630 ResponseBuilder { response: self }
631 }
632
633 /// Creates a [`ResponseBuilder`]
634 pub fn builder() -> ResponseBuilder {
635 ResponseBuilder::new(200)
636 }
637}
638
639impl std::fmt::Debug for Response {
640 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
641 f.debug_struct("Response")
642 .field("status", &self.status)
643 .field("headers", &self.headers)
644 .field("body.len()", &self.body.len())
645 .finish()
646 }
647}
648
649/// A builder for `Response``
650pub struct ResponseBuilder {
651 response: Response,
652}
653
654impl ResponseBuilder {
655 /// Create a new `ResponseBuilder`
656 pub fn new(status: impl conversions::IntoStatusCode) -> Self {
657 ResponseBuilder {
658 response: Response::new(status, Vec::new()),
659 }
660 }
661
662 /// Set the status
663 pub fn status(&mut self, status: impl conversions::IntoStatusCode) -> &mut Self {
664 self.response.status = status.into_status_code();
665 self
666 }
667
668 /// Set the headers
669 pub fn headers(&mut self, headers: impl conversions::IntoHeaders) -> &mut Self {
670 self.response.headers = into_header_rep(headers.into_headers());
671 self
672 }
673
674 /// Set a header
675 pub fn header(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
676 self.response
677 .headers
678 .insert(key.into().to_lowercase(), HeaderValue::string(value.into()));
679 self
680 }
681
682 /// Set the body
683 pub fn body(&mut self, body: impl conversions::IntoBody) -> &mut Self {
684 self.response.body = body.into_body();
685 self
686 }
687
688 /// Build the `Response`
689 pub fn build(&mut self) -> Response {
690 std::mem::replace(&mut self.response, Response::new(200, Vec::new()))
691 }
692}
693
694/// A header value.
695///
696/// Since header values do not have to be valid utf8, this allows for
697/// both utf8 strings and bags of bytes.
698#[derive(Debug, PartialEq, Eq, Clone)]
699pub struct HeaderValue {
700 inner: HeaderValueRep,
701}
702
703#[derive(Debug, PartialEq, Eq, Clone)]
704enum HeaderValueRep {
705 /// Header value encoded as a utf8 string
706 String(String),
707 /// Header value as a bag of bytes
708 Bytes(Vec<u8>),
709}
710
711impl HeaderValue {
712 /// Construct a `HeaderValue` from a string
713 pub fn string(str: String) -> HeaderValue {
714 HeaderValue {
715 inner: HeaderValueRep::String(str),
716 }
717 }
718
719 /// Construct a `HeaderValue` from a bag of bytes
720 pub fn bytes(bytes: Vec<u8>) -> HeaderValue {
721 HeaderValue {
722 inner: String::from_utf8(bytes)
723 .map(HeaderValueRep::String)
724 .unwrap_or_else(|e| HeaderValueRep::Bytes(e.into_bytes())),
725 }
726 }
727
728 /// Get the `HeaderValue` as a utf8 encoded string
729 ///
730 /// Returns `None` if the value is a non utf8 encoded header value
731 pub fn as_str(&self) -> Option<&str> {
732 match &self.inner {
733 HeaderValueRep::String(s) => Some(s),
734 HeaderValueRep::Bytes(b) => std::str::from_utf8(b).ok(),
735 }
736 }
737
738 /// Get the `HeaderValue` as bytes
739 pub fn as_bytes(&self) -> &[u8] {
740 self.as_ref()
741 }
742
743 /// Turn the `HeaderValue` into a String (in a lossy way if the `HeaderValue` is a bag of bytes)
744 pub fn into_utf8_lossy(self) -> String {
745 match self.inner {
746 HeaderValueRep::String(s) => s,
747 HeaderValueRep::Bytes(b) => String::from_utf8_lossy(&b).into_owned(),
748 }
749 }
750
751 /// Turn the `HeaderValue` into bytes
752 pub fn into_bytes(self) -> Vec<u8> {
753 match self.inner {
754 HeaderValueRep::String(s) => s.into_bytes(),
755 HeaderValueRep::Bytes(b) => b,
756 }
757 }
758}
759
760impl AsRef<[u8]> for HeaderValue {
761 fn as_ref(&self) -> &[u8] {
762 match &self.inner {
763 HeaderValueRep::String(s) => s.as_bytes(),
764 HeaderValueRep::Bytes(b) => b,
765 }
766 }
767}
768
769fn into_header_rep(headers: impl conversions::IntoHeaders) -> HashMap<String, HeaderValue> {
770 headers
771 .into_headers()
772 .into_iter()
773 .map(|(k, v)| {
774 let v = String::from_utf8(v)
775 .map(HeaderValueRep::String)
776 .unwrap_or_else(|e| HeaderValueRep::Bytes(e.into_bytes()));
777 (k.to_lowercase(), HeaderValue { inner: v })
778 })
779 .collect()
780}
781
782impl std::hash::Hash for Method {
783 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
784 core::mem::discriminant(self).hash(state);
785 }
786}
787
788impl Eq for Method {}
789
790impl PartialEq for Method {
791 fn eq(&self, other: &Self) -> bool {
792 match (self, other) {
793 (Self::Other(l), Self::Other(r)) => l == r,
794 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
795 }
796 }
797}
798
799impl std::fmt::Display for Method {
800 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
801 f.write_str(match self {
802 Method::Get => "GET",
803 Method::Post => "POST",
804 Method::Put => "PUT",
805 Method::Delete => "DELETE",
806 Method::Patch => "PATCH",
807 Method::Head => "HEAD",
808 Method::Options => "OPTIONS",
809 Method::Connect => "CONNECT",
810 Method::Trace => "TRACE",
811 Method::Other(o) => o,
812 })
813 }
814}
815
816impl IncomingRequest {
817 /// The incoming request Uri
818 pub fn uri(&self) -> String {
819 let scheme_and_authority =
820 if let (Some(scheme), Some(authority)) = (self.scheme(), self.authority()) {
821 let scheme = match &scheme {
822 Scheme::Http => "http://",
823 Scheme::Https => "https://",
824 Scheme::Other(s) => s.as_str(),
825 };
826 format!("{scheme}{authority}")
827 } else {
828 String::new()
829 };
830 let path_and_query = self.path_with_query().unwrap_or_default();
831 format!("{scheme_and_authority}{path_and_query}")
832 }
833
834 /// Return a `Stream` from which the body of the specified request may be read.
835 ///
836 /// # Panics
837 ///
838 /// Panics if the body was already consumed.
839 pub fn into_body_stream(self) -> impl futures::Stream<Item = Result<Vec<u8>, streams::Error>> {
840 executor::incoming_body(self.consume().expect("request body was already consumed"))
841 }
842
843 /// Return a `Vec<u8>` of the body or fails
844 pub async fn into_body(self) -> Result<Vec<u8>, streams::Error> {
845 use futures::TryStreamExt;
846 let mut stream = self.into_body_stream();
847 let mut body = Vec::new();
848 while let Some(chunk) = stream.try_next().await? {
849 body.extend(chunk);
850 }
851 Ok(body)
852 }
853}
854
855impl IncomingResponse {
856 /// Return a `Stream` from which the body of the specified response may be read.
857 ///
858 /// # Panics
859 ///
860 /// Panics if the body was already consumed.
861 // TODO: This should ideally take ownership of `self` and be called `into_body_stream` (i.e. symmetric with
862 // `IncomingRequest::into_body_stream`). However, as of this writing, `wasmtime-wasi-http` is implemented in
863 // such a way that dropping an `IncomingResponse` will cause the request to be cancelled, meaning the caller
864 // won't necessarily have a chance to send the request body if they haven't started doing so yet (or, if they
865 // have started, they might not be able to finish before the connection is closed). See
866 // https://github.com/bytecodealliance/wasmtime/issues/7413 for details.
867 pub fn take_body_stream(&self) -> impl futures::Stream<Item = Result<Vec<u8>, streams::Error>> {
868 executor::incoming_body(self.consume().expect("response body was already consumed"))
869 }
870
871 /// Return a `Vec<u8>` of the body or fails
872 ///
873 /// # Panics
874 ///
875 /// Panics if the body was already consumed.
876 pub async fn into_body(self) -> Result<Vec<u8>, streams::Error> {
877 use futures::TryStreamExt;
878 let mut stream = self.take_body_stream();
879 let mut body = Vec::new();
880 while let Some(chunk) = stream.try_next().await? {
881 body.extend(chunk);
882 }
883 Ok(body)
884 }
885}
886
887impl OutgoingResponse {
888 /// Construct a `Sink` which writes chunks to the body of the specified response.
889 ///
890 /// # Panics
891 ///
892 /// Panics if the body was already taken.
893 pub fn take_body(&self) -> impl futures::Sink<Vec<u8>, Error = StreamError> {
894 executor::outgoing_body(self.body().expect("response body was already taken"))
895 }
896}
897
898impl OutgoingRequest {
899 /// Construct a `Sink` which writes chunks to the body of the specified response.
900 ///
901 /// # Panics
902 ///
903 /// Panics if the body was already taken.
904 pub fn take_body(&self) -> impl futures::Sink<Vec<u8>, Error = StreamError> {
905 executor::outgoing_body(self.body().expect("request body was already taken"))
906 }
907}
908
909/// A parameter provided by Spin for setting a streaming [OutgoingResponse].
910///
911/// # Examples
912///
913/// Send a streaming response to an incoming request:
914///
915/// ```no_run
916/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse, ResponseOutparam};
917/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) {
918/// use futures::SinkExt;
919/// use std::io::Read;
920///
921/// let response_headers = Fields::from_list(&[
922/// ("content-type".to_owned(), "text/plain".into())
923/// ]).unwrap();
924///
925/// let response = OutgoingResponse::new(response_headers);
926/// response.set_status_code(200).unwrap();
927/// let mut response_body = response.take_body();
928///
929/// response_outparam.set(response);
930///
931/// let mut file = std::fs::File::open("war-and-peace.txt").unwrap();
932///
933/// loop {
934/// let mut buf = vec![0; 1024];
935/// let count = file.read(&mut buf).unwrap();
936///
937/// if count == 0 {
938/// break; // end of file
939/// }
940///
941/// buf.truncate(count);
942/// response_body.send(buf).await.unwrap();
943/// }
944/// }
945/// ```
946pub struct ResponseOutparam(types::ResponseOutparam);
947
948impl ResponseOutparam {
949 #[doc(hidden)]
950 // This is needed for the macro so we can transfrom the macro's
951 // `ResponseOutparam` to this `ResponseOutparam`
952 pub unsafe fn from_handle(handle: u32) -> Self {
953 Self(types::ResponseOutparam::from_handle(handle))
954 }
955
956 /// Set the outgoing response
957 pub fn set(self, response: OutgoingResponse) {
958 types::ResponseOutparam::set(self.0, Ok(response));
959 }
960
961 /// Set with the outgoing response and the supplied buffer
962 ///
963 /// Will panic if response body has already been taken
964 pub async fn set_with_body(
965 self,
966 response: OutgoingResponse,
967 buffer: Vec<u8>,
968 ) -> Result<(), StreamError> {
969 let mut body = response.take_body();
970 self.set(response);
971 body.send(buffer).await
972 }
973
974 /// Return the inner, `wit-bindgen`-generated instance
975 pub fn into_inner(self) -> types::ResponseOutparam {
976 self.0
977 }
978}
979
980/// Send an outgoing request
981///
982/// # Examples
983///
984/// Get the example.com home page:
985///
986/// ```no_run
987/// use spin_sdk::http::{Request, Response};
988///
989/// # #[tokio::main]
990/// # async fn main() -> anyhow::Result<()> {
991/// let request = Request::get("example.com").build();
992/// let response: Response = spin_sdk::http::send(request).await?;
993/// println!("{}", response.body().len());
994/// # Ok(())
995/// # }
996/// ```
997///
998/// Use the `http` crate Request type to send a data transfer value:
999///
1000/// ```no_run
1001/// use hyperium::Request;
1002///
1003/// #[derive(serde::Serialize)]
1004/// struct User {
1005/// name: String,
1006/// }
1007///
1008/// impl spin_sdk::http::conversions::TryIntoBody for User {
1009/// type Error = serde_json::Error;
1010///
1011/// fn try_into_body(self) -> Result<Vec<u8>, Self::Error> {
1012/// serde_json::to_vec(&self)
1013/// }
1014/// }
1015///
1016/// # #[tokio::main]
1017/// # async fn main() -> anyhow::Result<()> {
1018/// let user = User {
1019/// name: "Alice".to_owned(),
1020/// };
1021///
1022/// let request = hyperium::Request::builder()
1023/// .method("POST")
1024/// .uri("https://example.com/users")
1025/// .header("content-type", "application/json")
1026/// .body(user)?;
1027///
1028/// let response: hyperium::Response<()> = spin_sdk::http::send(request).await?;
1029///
1030/// println!("{}", response.status().is_success());
1031/// # Ok(())
1032/// # }
1033/// ```
1034#[doc(alias = "fetch")]
1035pub async fn send<I, O>(request: I) -> Result<O, SendError>
1036where
1037 I: TryIntoOutgoingRequest,
1038 I::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
1039 O: TryFromIncomingResponse,
1040 O::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
1041{
1042 let (request, body_buffer) = I::try_into_outgoing_request(request)
1043 .map_err(|e| SendError::RequestConversion(e.into()))?;
1044 let response = if let Some(body_buffer) = body_buffer {
1045 // It is part of the contract of the trait that implementors of `TryIntoOutgoingRequest`
1046 // do not call `OutgoingRequest::write`` if they return a buffered body.
1047 let mut body_sink = request.take_body();
1048 let response = executor::outgoing_request_send(request);
1049 body_sink.send(body_buffer).await.map_err(SendError::Io)?;
1050 drop(body_sink);
1051 response.await.map_err(SendError::Http)?
1052 } else {
1053 executor::outgoing_request_send(request)
1054 .await
1055 .map_err(SendError::Http)?
1056 };
1057
1058 TryFromIncomingResponse::try_from_incoming_response(response)
1059 .await
1060 .map_err(|e: O::Error| SendError::ResponseConversion(e.into()))
1061}
1062
1063/// An error encountered when performing an HTTP request
1064#[derive(thiserror::Error, Debug)]
1065pub enum SendError {
1066 /// Error converting to a request
1067 #[error(transparent)]
1068 RequestConversion(Box<dyn std::error::Error + Send + Sync>),
1069 /// Error converting from a response
1070 #[error(transparent)]
1071 ResponseConversion(Box<dyn std::error::Error + Send + Sync>),
1072 /// An I/O error
1073 #[error(transparent)]
1074 Io(StreamError),
1075 /// An HTTP error
1076 #[error(transparent)]
1077 Http(ErrorCode),
1078}
1079
1080#[doc(hidden)]
1081/// The executor for driving wasi-http futures to completion
1082mod executor;
1083#[doc(hidden)]
1084pub use executor::run;
1085
1086/// An error parsing a JSON body
1087#[cfg(feature = "json")]
1088#[derive(Debug)]
1089pub struct JsonBodyError(serde_json::Error);
1090
1091#[cfg(feature = "json")]
1092impl std::error::Error for JsonBodyError {}
1093
1094#[cfg(feature = "json")]
1095impl std::fmt::Display for JsonBodyError {
1096 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1097 f.write_str("could not convert body to json")
1098 }
1099}
1100
1101/// An error when the body is not UTF-8
1102#[derive(Debug)]
1103pub struct NonUtf8BodyError;
1104
1105impl std::error::Error for NonUtf8BodyError {}
1106
1107impl std::fmt::Display for NonUtf8BodyError {
1108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1109 f.write_str("body was expected to be utf8 but was not")
1110 }
1111}
1112
1113mod router;
1114#[doc(inline)]
1115pub use router::*;
1116
1117/// A Body extractor
1118#[derive(Debug)]
1119pub struct Body<T>(pub T);
1120
1121impl<T> std::ops::Deref for Body<T> {
1122 type Target = T;
1123
1124 fn deref(&self) -> &Self::Target {
1125 &self.0
1126 }
1127}
1128
1129/// A Json extractor
1130#[derive(Debug)]
1131pub struct Json<T>(pub T);
1132
1133impl<T> std::ops::Deref for Json<T> {
1134 type Target = T;
1135
1136 fn deref(&self) -> &Self::Target {
1137 &self.0
1138 }
1139}
1140
1141/// Helper functions for creating responses
1142pub mod responses {
1143 use super::Response;
1144
1145 /// Helper function to return a 404 Not Found response.
1146 pub fn not_found() -> Response {
1147 Response::new(404, "Not Found")
1148 }
1149
1150 /// Helper function to return a 500 Internal Server Error response.
1151 pub fn internal_server_error() -> Response {
1152 Response::new(500, "Internal Server Error")
1153 }
1154
1155 /// Helper function to return a 405 Method Not Allowed response.
1156 pub fn method_not_allowed() -> Response {
1157 Response::new(405, "Method Not Allowed")
1158 }
1159
1160 pub(crate) fn bad_request(msg: Option<String>) -> Response {
1161 Response::new(400, msg.map(|m| m.into_bytes()))
1162 }
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167 use super::*;
1168
1169 #[test]
1170 fn request_uri_parses() {
1171 let uri = "/hello?world=1";
1172 let req = Request::new(Method::Get, uri);
1173 assert_eq!(req.uri(), uri);
1174 assert_eq!(req.path(), "/hello");
1175 assert_eq!(req.query(), "world=1");
1176
1177 let uri = "http://localhost:3000/hello?world=1";
1178 let req = Request::new(Method::Get, uri);
1179 assert_eq!(req.uri(), uri);
1180 assert_eq!(req.path(), "/hello");
1181 assert_eq!(req.query(), "world=1");
1182
1183 let uri = "localhost:3000/hello?world=1";
1184 let req = Request::new(Method::Get, uri);
1185 assert_eq!(req.uri(), uri);
1186 assert_eq!(req.path(), "/hello");
1187 assert_eq!(req.query(), "world=1");
1188 }
1189}