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