diff --git a/test-programs/src/bin/client_get_headers.rs b/test-programs/src/bin/client_get_headers.rs new file mode 100644 index 0000000..82989fc --- /dev/null +++ b/test-programs/src/bin/client_get_headers.rs @@ -0,0 +1,27 @@ +use serde::Deserialize; +use std::collections::HashMap; +use waki::{header::CONTENT_TYPE, Client}; + +#[derive(Deserialize)] +struct Data { + headers: HashMap, +} + +fn main() { + let resp = Client::new() + .get("https://httpbin.org/get") + .header("Test", "test") + .headers([("A", "b"), ("C", "d")]) + .send() + .unwrap(); + assert_eq!(resp.status_code(), 200); + assert_eq!( + resp.header(CONTENT_TYPE).unwrap().to_str().unwrap(), + "application/json" + ); + + let data = resp.json::().unwrap(); + assert_eq!(data.headers.get("Test").unwrap(), "test"); + assert_eq!(data.headers.get("A").unwrap(), "b"); + assert_eq!(data.headers.get("C").unwrap(), "d"); +} diff --git a/test-programs/src/bin/client_get_with_query.rs b/test-programs/src/bin/client_get_with_query.rs index ac6e50c..71b2278 100644 --- a/test-programs/src/bin/client_get_with_query.rs +++ b/test-programs/src/bin/client_get_with_query.rs @@ -10,7 +10,6 @@ struct Data { fn main() { let resp = Client::new() .get("https://httpbin.org/get?a=b") - .headers([("Content-Type", "application/json"), ("Accept", "*/*")]) .send() .unwrap(); assert_eq!(resp.status_code(), 200); diff --git a/waki-macros/src/export.rs b/waki-macros/src/export.rs index 059221e..3a434b8 100644 --- a/waki-macros/src/export.rs +++ b/waki-macros/src/export.rs @@ -16,8 +16,11 @@ pub fn handler(input: ItemFn) -> Result { impl ::waki::bindings::exports::wasi::http::incoming_handler::Guest for Component { fn handle(request: ::waki::bindings::wasi::http::types::IncomingRequest, response_out: ::waki::bindings::wasi::http::types::ResponseOutparam) { - match #fn_name(request.into()) { - Ok(resp) => ::waki::handle_response(response_out, resp), + match request.try_into() { + Ok(req) => match #fn_name(req) { + Ok(resp) => ::waki::handle_response(response_out, resp), + Err(e) => ::waki::bindings::wasi::http::types::ResponseOutparam::set(response_out, Err(e)), + } Err(e) => ::waki::bindings::wasi::http::types::ResponseOutparam::set(response_out, Err(e)), } } diff --git a/waki/Cargo.toml b/waki/Cargo.toml index f88beff..1a3ab2f 100644 --- a/waki/Cargo.toml +++ b/waki/Cargo.toml @@ -21,6 +21,7 @@ anyhow.workspace = true serde.workspace = true wit-bindgen = "0.26.0" url = "2.5.0" +http = "1.1.0" serde_urlencoded = "0.7.1" serde_json = { version = "1.0.117", optional = true } mime = { version = "0.3.17", optional = true } diff --git a/waki/src/common/header.rs b/waki/src/common/header.rs new file mode 100644 index 0000000..ff95a1d --- /dev/null +++ b/waki/src/common/header.rs @@ -0,0 +1,34 @@ +use crate::{ + bindings::wasi::http::types::{HeaderError, Headers, IncomingRequest, IncomingResponse}, + header::HeaderMap, +}; +use anyhow::Result; + +macro_rules! impl_header { + ($($t:ty),+ $(,)?) => ($( + impl $t { + pub fn headers_map(&self) -> Result { + let headers_handle = self.headers(); + headers_handle + .entries() + .into_iter() + .map(|(key, value)| Ok((key.try_into()?, value.try_into()?))) + .collect::>() + } + } + )+) +} + +impl_header!(IncomingRequest, IncomingResponse); + +impl TryFrom for Headers { + type Error = HeaderError; + + fn try_from(headers: HeaderMap) -> Result { + let entries = headers + .iter() + .map(|(k, v)| (k.to_string(), v.as_bytes().into())) + .collect::>(); + Headers::from_list(&entries) + } +} diff --git a/waki/src/common/mod.rs b/waki/src/common/mod.rs new file mode 100644 index 0000000..6064458 --- /dev/null +++ b/waki/src/common/mod.rs @@ -0,0 +1,2 @@ +mod header; +mod request_and_response; diff --git a/waki/src/common/request_and_response.rs b/waki/src/common/request_and_response.rs new file mode 100644 index 0000000..dec3fd4 --- /dev/null +++ b/waki/src/common/request_and_response.rs @@ -0,0 +1,290 @@ +#[cfg(feature = "multipart")] +use crate::multipart::{parser::parse, Form, Part}; +use crate::{ + body::Body, + header::{AsHeaderName, HeaderMap, HeaderValue, IntoHeaderName, CONTENT_TYPE}, + Request, RequestBuilder, Response, ResponseBuilder, +}; +use anyhow::{anyhow, Error, Result}; +use serde::Serialize; +use std::collections::HashMap; + +macro_rules! impl_common_get_methods { + ($($t:ty),+ $(,)?) => ($( + impl $t { + /// Get the header. + pub fn header(&self, key: K) -> Option<&HeaderValue> { + self.headers.get(key) + } + + /// Get headers. + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Get a chunk of the body. + /// + /// It will block until at least one byte can be read or the stream is closed. + /// + /// NOTE: This method is only for incoming requests/responses, if you call it on an + /// outgoing request/response it will always return None. + pub fn chunk(&self, len: u64) -> Result>> { + self.body.chunk(len) + } + + /// Get the full body. + /// + /// It will block until the stream is closed. + pub fn body(self) -> Result> { + self.body.bytes() + } + + /// Deserialize the body as JSON. + /// + /// # Optional + /// + /// This requires the `json` feature enabled. + /// + /// ``` + /// # use anyhow::Result; + /// # use serde::Deserialize; + /// # use waki::Response; + /// # fn run() -> Result<()> { + /// # let r = Response::new(); + /// #[derive(Deserialize)] + /// struct Data { + /// origin: String, + /// url: String, + /// } + /// + /// let json_data = r.json::()?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] + pub fn json(self) -> Result { + Ok(serde_json::from_slice(self.body()?.as_ref())?) + } + + /// Parse the body as multipart/form-data. + /// + /// # Optional + /// + /// This requires the `multipart` feature enabled. + #[cfg(feature = "multipart")] + #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] + pub fn multipart(self) -> Result> { + match self.headers.get(CONTENT_TYPE) { + Some(header) => { + let mime = header.to_str()?.parse::()?; + let boundary = match mime.get_param(mime::BOUNDARY) { + Some(v) => v.as_str(), + None => { + return Err(anyhow!( + "unable to find the boundary value in the Content-Type header" + )) + } + }; + parse(self.body()?.as_ref(), boundary) + } + None => Err(anyhow!( + "parse body as multipart failed, unable to find the Content-Type header" + )), + } + } + } + )+) +} + +impl_common_get_methods!(Request, Response); + +macro_rules! impl_common_set_methods { + ($($t:ty),+ $(,)?) => ($( + impl $t { + /// Add a header. + /// + /// ``` + /// # use waki::ResponseBuilder; + /// # fn run() { + /// # let r = ResponseBuilder::new(); + /// r.header("Content-Type", "application/json"); + /// # } + /// ``` + pub fn header(mut self, key: K, value: V) -> Self + where + K: IntoHeaderName, + V: TryInto, + >::Error: Into, + { + let mut err = None; + if let Ok(ref mut inner) = self.inner { + match value.try_into().map_err(|e| e.into()) { + Ok(v) => { + inner.headers.insert(key, v); + } + Err(e) => err = Some(e), + }; + } + if let Some(e) = err { + self.inner = Err(e); + } + self + } + + /// Add a set of headers. + /// + /// ``` + /// # use waki::ResponseBuilder; + /// # fn run() { + /// # let r = ResponseBuilder::new(); + /// r.headers([("Content-Type", "application/json"), ("Accept", "*/*")]); + /// # } + /// ``` + pub fn headers(mut self, headers: I) -> Self + where + K: IntoHeaderName, + V: TryInto, + >::Error: Into, + I: IntoIterator, + { + let mut err = None; + if let Ok(ref mut inner) = self.inner { + for (key, value) in headers.into_iter() { + match value.try_into().map_err(|e| e.into()) { + Ok(v) => { + inner.headers.insert(key, v); + } + Err(e) => { + err = Some(e); + break; + } + }; + } + } + if let Some(e) = err { + self.inner = Err(e); + } + self + } + + /// Set the body. + /// + /// ``` + /// # use waki::ResponseBuilder; + /// # fn run() { + /// # let r = ResponseBuilder::new(); + /// r.body("hello"); + /// # } + /// ``` + pub fn body>>(mut self, body: V) -> Self { + if let Ok(ref mut inner) = self.inner { + inner.body = Body::Bytes(body.into()); + } + self + } + + /// Set a JSON body. + /// + /// # Optional + /// + /// This requires the `json` feature enabled. + /// + /// ``` + /// # use std::collections::HashMap; + /// # use waki::ResponseBuilder; + /// # fn run() { + /// # let r = ResponseBuilder::new(); + /// r.json(&HashMap::from([("data", "hello")])); + /// # } + /// ``` + #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] + pub fn json(mut self, json: &T) -> Self { + let mut err = None; + if let Ok(ref mut inner) = self.inner { + inner.headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); + match serde_json::to_vec(json) { + Ok(data) => inner.body = Body::Bytes(data), + Err(e) => err = Some(e.into()), + } + } + if let Some(e) = err { + self.inner = Err(e); + } + self + } + + /// Set a form body. + /// + /// ``` + /// # use waki::ResponseBuilder; + /// # fn run() { + /// # let r = ResponseBuilder::new(); + /// r.form(&[("a", "b"), ("c", "d")]); + /// # } + /// ``` + pub fn form(mut self, form: &T) -> Self { + let mut err = None; + if let Ok(ref mut inner) = self.inner { + inner.headers.insert( + CONTENT_TYPE, + "application/x-www-form-urlencoded".parse().unwrap(), + ); + match serde_urlencoded::to_string(form) { + Ok(data) => inner.body = Body::Bytes(data.into()), + Err(e) => err = Some(e.into()), + } + } + if let Some(e) = err { + self.inner = Err(e); + } + self + } + + /// Set a multipart/form-data body. + /// + /// # Optional + /// + /// This requires the `multipart` feature enabled. + /// + /// ``` + /// # use anyhow::Result; + /// # use waki::ResponseBuilder; + /// # fn run() -> Result<()> { + /// # let r = ResponseBuilder::new(); + /// let form = waki::multipart::Form::new() + /// // Add a text field + /// .text("key", "value") + /// // And a file + /// .file("file", "/path/to/file.txt")? + /// // And a custom part + /// .part( + /// waki::multipart::Part::new("key2", "value2") + /// .filename("file.txt") + /// .mime_str("text/plain")?, + /// ); + /// + /// r.multipart(form); + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "multipart")] + #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] + pub fn multipart(mut self, form: Form) -> Self { + if let Ok(ref mut inner) = self.inner { + inner.headers.insert( + CONTENT_TYPE, + format!("multipart/form-data; boundary={}", form.boundary()) + .parse() + .unwrap(), + ); + inner.body = Body::Bytes(form.build()); + } + self + } + } + )+) +} + +impl_common_set_methods!(RequestBuilder, ResponseBuilder); diff --git a/waki/src/header.rs b/waki/src/header.rs deleted file mode 100644 index 4ceb53c..0000000 --- a/waki/src/header.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::bindings::wasi::http::types::{HeaderError, Headers, IncomingRequest, IncomingResponse}; - -use std::collections::HashMap; - -macro_rules! impl_header { - ($($t:ty),+ $(,)?) => ($( - impl $t { - pub fn headers_map(&self) -> HashMap { - let headers_handle = self.headers(); - headers_handle - .entries() - .into_iter() - .map(|(key, value)| (key, String::from_utf8_lossy(&value).to_string())) - .collect() - } - } - )+) -} - -impl_header!(IncomingRequest, IncomingResponse); - -impl TryFrom> for Headers { - type Error = HeaderError; - - fn try_from(headers: HashMap) -> Result { - let entries = headers - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect::>(); - Headers::from_list(&entries) - } -} diff --git a/waki/src/lib.rs b/waki/src/lib.rs index 0bec5f5..cef0fef 100644 --- a/waki/src/lib.rs +++ b/waki/src/lib.rs @@ -34,7 +34,7 @@ mod body; mod client; -mod header; +mod common; #[cfg(feature = "multipart")] #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] pub mod multipart; @@ -75,254 +75,4 @@ pub use self::{ /// ``` pub use waki_macros::handler; -use crate::body::Body; -#[cfg(feature = "multipart")] -use crate::multipart::{parser::parse, Form, Part}; -use anyhow::{anyhow, Result}; -use serde::Serialize; -use std::collections::HashMap; - -macro_rules! impl_common_get_methods { - ($($t:ty),+ $(,)?) => ($( - impl $t { - /// Get the headers. - pub fn headers(&self) -> &HashMap { - &self.headers - } - - /// Get a chunk of the body. - /// - /// It will block until at least one byte can be read or the stream is closed. - /// - /// NOTE: This method is only for incoming requests/responses, if you call it on an - /// outgoing request/response it will always return None. - pub fn chunk(&self, len: u64) -> Result>> { - self.body.chunk(len) - } - - /// Get the full body. - /// - /// It will block until the stream is closed. - pub fn body(self) -> Result> { - self.body.bytes() - } - - /// Deserialize the body as JSON. - /// - /// # Optional - /// - /// This requires the `json` feature enabled. - /// - /// ``` - /// # use anyhow::Result; - /// # use serde::Deserialize; - /// # use waki::Response; - /// # fn run() -> Result<()> { - /// # let r = Response::new(); - /// #[derive(Deserialize)] - /// struct Data { - /// origin: String, - /// url: String, - /// } - /// - /// let json_data = r.json::()?; - /// # Ok(()) - /// # } - /// ``` - #[cfg(feature = "json")] - #[cfg_attr(docsrs, doc(cfg(feature = "json")))] - pub fn json(self) -> Result { - Ok(serde_json::from_slice(self.body()?.as_ref())?) - } - - /// Parse the body as multipart/form-data. - /// - /// # Optional - /// - /// This requires the `multipart` feature enabled. - #[cfg(feature = "multipart")] - #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] - pub fn multipart(self) -> Result> { - match self.headers.get("Content-Type") { - Some(header) => { - let mime = header.parse::()?; - let boundary = match mime.get_param(mime::BOUNDARY) { - Some(v) => v.as_str(), - None => { - return Err(anyhow!( - "unable to find the boundary value in the Content-Type header" - )) - } - }; - parse(self.body()?.as_ref(), boundary) - } - None => Err(anyhow!( - "parse body as multipart failed, unable to find the Content-Type header" - )), - } - } - } - )+) -} - -impl_common_get_methods!(Request, Response); - -macro_rules! impl_common_set_methods { - ($($t:ty),+ $(,)?) => ($( - impl $t { - /// Add a header. - /// - /// ``` - /// # use waki::ResponseBuilder; - /// # fn run() { - /// # let r = ResponseBuilder::new(); - /// r.header("Content-Type", "application/json"); - /// # } - /// ``` - pub fn header>(mut self, key: S, value: S) -> Self { - if let Ok(ref mut inner) = self.inner { - inner.headers.insert(key.into(), value.into()); - } - self - } - - /// Add a set of headers. - /// - /// ``` - /// # use waki::ResponseBuilder; - /// # fn run() { - /// # let r = ResponseBuilder::new(); - /// r.headers([("Content-Type", "application/json"), ("Accept", "*/*")]); - /// # } - /// ``` - pub fn headers(mut self, headers: I) -> Self - where - S: Into, - I: IntoIterator, - { - if let Ok(ref mut inner) = self.inner { - inner.headers - .extend(headers.into_iter().map(|(k, v)| (k.into(), v.into()))); - } - self - } - - /// Set the body. - /// - /// ``` - /// # use waki::ResponseBuilder; - /// # fn run() { - /// # let r = ResponseBuilder::new(); - /// r.body("hello"); - /// # } - /// ``` - pub fn body>>(mut self, body: V) -> Self { - if let Ok(ref mut inner) = self.inner { - inner.body = Body::Bytes(body.into()); - } - self - } - - /// Set a JSON body. - /// - /// # Optional - /// - /// This requires the `json` feature enabled. - /// - /// ``` - /// # use std::collections::HashMap; - /// # use waki::ResponseBuilder; - /// # fn run() { - /// # let r = ResponseBuilder::new(); - /// r.json(&HashMap::from([("data", "hello")])); - /// # } - /// ``` - #[cfg(feature = "json")] - #[cfg_attr(docsrs, doc(cfg(feature = "json")))] - pub fn json(mut self, json: &T) -> Self { - let mut err = None; - if let Ok(ref mut inner) = self.inner { - inner.headers - .insert("Content-Type".into(), "application/json".into()); - match serde_json::to_vec(json) { - Ok(data) => inner.body = Body::Bytes(data), - Err(e) => err = Some(e.into()), - } - } - if let Some(e) = err { - self.inner = Err(e); - } - self - } - - /// Set a form body. - /// - /// ``` - /// # use waki::ResponseBuilder; - /// # fn run() { - /// # let r = ResponseBuilder::new(); - /// r.form(&[("a", "b"), ("c", "d")]); - /// # } - /// ``` - pub fn form(mut self, form: &T) -> Self { - let mut err = None; - if let Ok(ref mut inner) = self.inner { - inner.headers.insert( - "Content-Type".into(), - "application/x-www-form-urlencoded".into(), - ); - match serde_urlencoded::to_string(form) { - Ok(data) => inner.body = Body::Bytes(data.into()), - Err(e) => err = Some(e.into()), - } - } - if let Some(e) = err { - self.inner = Err(e); - } - self - } - - /// Set a multipart/form-data body. - /// - /// # Optional - /// - /// This requires the `multipart` feature enabled. - /// - /// ``` - /// # use anyhow::Result; - /// # use waki::ResponseBuilder; - /// # fn run() -> Result<()> { - /// # let r = ResponseBuilder::new(); - /// let form = waki::multipart::Form::new() - /// // Add a text field - /// .text("key", "value") - /// // And a file - /// .file("file", "/path/to/file.txt")? - /// // And a custom part - /// .part( - /// waki::multipart::Part::new("key2", "value2") - /// .filename("file.txt") - /// .mime_str("text/plain")?, - /// ); - /// - /// r.multipart(form); - /// # Ok(()) - /// # } - /// ``` - #[cfg(feature = "multipart")] - #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] - pub fn multipart(mut self, form: Form) -> Self { - if let Ok(ref mut inner) = self.inner { - inner.headers.insert( - "Content-Type".into(), - format!("multipart/form-data; boundary={}", form.boundary()), - ); - inner.body = Body::Bytes(form.build()); - } - self - } - } - )+) -} - -impl_common_set_methods!(RequestBuilder, ResponseBuilder); +pub use http::header; diff --git a/waki/src/multipart/mod.rs b/waki/src/multipart/mod.rs index 32df257..70140d0 100644 --- a/waki/src/multipart/mod.rs +++ b/waki/src/multipart/mod.rs @@ -1,10 +1,11 @@ mod constants; pub(crate) mod parser; -use anyhow::Result; +use crate::header::{HeaderMap, HeaderValue, IntoHeaderName, CONTENT_DISPOSITION, CONTENT_TYPE}; + +use anyhow::{Error, Result}; use mime::Mime; use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::Path; @@ -60,10 +61,11 @@ impl Form { for part in self.parts { buf.extend_from_slice( format!( - "{}{}{}Content-Disposition: form-data; name={}", + "{}{}{}{}: form-data; name={}", constants::BOUNDARY_EXT, self.boundary, constants::CRLF, + CONTENT_DISPOSITION, part.key ) .as_bytes(), @@ -73,11 +75,12 @@ impl Form { } if let Some(mime) = part.mime { buf.extend_from_slice( - format!("{}Content-Type: {}", constants::CRLF, mime).as_bytes(), + format!("{}{}: {}", constants::CRLF, CONTENT_TYPE, mime).as_bytes(), ); } - for (k, v) in part.headers { - buf.extend_from_slice(format!("{}{}: {}", constants::CRLF, k, v).as_bytes()); + for (k, v) in part.headers.iter() { + buf.extend_from_slice(format!("{}{}: ", constants::CRLF, k).as_bytes()); + buf.extend_from_slice(v.as_bytes()); } buf.extend_from_slice(constants::CRLF_CRLF.as_bytes()); @@ -110,7 +113,7 @@ pub struct Part { pub value: Vec, pub filename: Option, pub mime: Option, - pub headers: HashMap, + pub headers: HeaderMap, } impl Part { @@ -124,7 +127,7 @@ impl Part { value: value.into(), filename: None, mime: None, - headers: HashMap::new(), + headers: HeaderMap::new(), } } @@ -164,13 +167,17 @@ impl Part { self } - pub fn headers(mut self, headers: I) -> Self + pub fn headers(mut self, headers: I) -> Result where - S: Into, - I: IntoIterator, + K: IntoHeaderName, + V: TryInto, + >::Error: Into, + I: IntoIterator, { - self.headers - .extend(headers.into_iter().map(|(k, v)| (k.into(), v.into()))); - self + for (key, value) in headers.into_iter() { + self.headers + .insert(key, value.try_into().map_err(|e| e.into())?); + } + Ok(self) } } diff --git a/waki/src/multipart/parser.rs b/waki/src/multipart/parser.rs index 26494e4..ad89233 100644 --- a/waki/src/multipart/parser.rs +++ b/waki/src/multipart/parser.rs @@ -1,4 +1,7 @@ -use super::{constants, Part}; +use crate::{ + header::{HeaderMap, HeaderName, HeaderValue, CONTENT_DISPOSITION, CONTENT_TYPE}, + multipart::{constants, Part}, +}; use anyhow::{anyhow, Result}; use bytes::{Buf, Bytes, BytesMut}; @@ -57,15 +60,15 @@ pub fn parse(body: &[u8], boundary: &str) -> Result> { let mut headers = [httparse::EMPTY_HEADER; constants::MAX_HEADERS]; part.headers = match httparse::parse_headers(&header_bytes, &mut headers)? { Status::Complete((_, raw_headers)) => { - let mut headers_map = HashMap::new(); + let mut headers_map = HeaderMap::with_capacity(raw_headers.len()); for header in raw_headers { let (k, v) = ( - header.name.to_string(), - String::from_utf8(header.value.to_vec())?, + HeaderName::try_from(header.name)?, + HeaderValue::try_from(header.value)?, ); - if k.to_uppercase() == "Content-Disposition".to_uppercase() { + if k == CONTENT_DISPOSITION { // can't parse it without a / - let mime = format!("multipart/{}", v).parse::()?; + let mime = format!("multipart/{}", v.to_str()?).parse::()?; part.key = match mime.get_param("name") { Some(name) => name.to_string(), None => { @@ -76,8 +79,8 @@ pub fn parse(body: &[u8], boundary: &str) -> Result> { }; part.filename = mime.get_param("filename").map(|v| v.to_string()); }; - if k.to_uppercase() == "Content-Type".to_uppercase() { - part.mime = Some(v.parse()?) + if k == CONTENT_TYPE { + part.mime = Some(v.to_str()?.parse()?) } headers_map.insert(k, v); } diff --git a/waki/src/request.rs b/waki/src/request.rs index 75803cd..a905a86 100644 --- a/waki/src/request.rs +++ b/waki/src/request.rs @@ -4,7 +4,8 @@ use crate::{ types::{IncomingRequest, OutgoingBody, OutgoingRequest, RequestOptions, Scheme}, }, body::Body, - Method, Response, + header::HeaderMap, + ErrorCode, Method, Response, }; use anyhow::{anyhow, Error, Result}; @@ -44,11 +45,11 @@ impl RequestBuilder { let mut pairs = req.url.query_pairs_mut(); let serializer = serde_urlencoded::Serializer::new(&mut pairs); if let Err(e) = query.serialize(serializer) { - err = Some(e); + err = Some(e.into()); } } if let Some(e) = err { - self.inner = Err(e.into()); + self.inner = Err(e); } self } @@ -90,13 +91,15 @@ impl RequestBuilder { pub struct Request { method: Method, url: Url, - pub(crate) headers: HashMap, + pub(crate) headers: HeaderMap, pub(crate) body: Body, connect_timeout: Option, } -impl From for Request { - fn from(req: IncomingRequest) -> Self { +impl TryFrom for Request { + type Error = ErrorCode; + + fn try_from(req: IncomingRequest) -> std::result::Result { let scheme = match req.scheme().unwrap_or(Scheme::Http) { Scheme::Http => "http".into(), Scheme::Https => "https".into(), @@ -111,18 +114,20 @@ impl From for Request { )) .unwrap(); - let headers = req.headers_map(); + let headers = req + .headers_map() + .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; // The consume() method can only be called once let incoming_body = req.consume().unwrap(); drop(req); - Self { + Ok(Self { method, url, headers, body: Body::Stream(incoming_body.into()), connect_timeout: None, - } + }) } } @@ -131,7 +136,7 @@ impl Request { Self { method, url, - headers: HashMap::new(), + headers: HeaderMap::new(), body: Body::Bytes(vec![]), connect_timeout: None, } @@ -217,6 +222,6 @@ impl Request { }?; drop(future_response); - Ok(incoming_response.into()) + incoming_response.try_into() } } diff --git a/waki/src/response.rs b/waki/src/response.rs index debf90d..968bb45 100644 --- a/waki/src/response.rs +++ b/waki/src/response.rs @@ -3,11 +3,11 @@ use crate::{ IncomingResponse, OutgoingBody, OutgoingResponse, ResponseOutparam, }, body::Body, + header::HeaderMap, ErrorCode, }; -use anyhow::Result; -use std::collections::HashMap; +use anyhow::{Error, Result}; pub struct ResponseBuilder { // all errors generated while building the response will be deferred. @@ -47,7 +47,7 @@ impl ResponseBuilder { } pub struct Response { - pub(crate) headers: HashMap, + pub(crate) headers: HeaderMap, pub(crate) body: Body, status_code: u16, } @@ -58,26 +58,28 @@ impl Default for Response { } } -impl From for Response { - fn from(incoming_response: IncomingResponse) -> Self { +impl TryFrom for Response { + type Error = Error; + + fn try_from(incoming_response: IncomingResponse) -> std::result::Result { let status_code = incoming_response.status(); - let headers = incoming_response.headers_map(); + let headers = incoming_response.headers_map()?; // The consume() method can only be called once let incoming_body = incoming_response.consume().unwrap(); drop(incoming_response); - Self { + Ok(Self { headers, status_code, body: Body::Stream(incoming_body.into()), - } + }) } } impl Response { pub fn new() -> Self { Self { - headers: HashMap::new(), + headers: HeaderMap::new(), status_code: 200, body: Body::Bytes(vec![]), } diff --git a/waki/tests/all/client.rs b/waki/tests/all/client.rs index 2a3cfbe..00eec12 100644 --- a/waki/tests/all/client.rs +++ b/waki/tests/all/client.rs @@ -7,6 +7,13 @@ async fn get_chunk() { .unwrap(); } +#[tokio::test(flavor = "multi_thread")] +async fn get_headers() { + run_wasi(test_programs_artifacts::CLIENT_GET_HEADERS_COMPONENT) + .await + .unwrap(); +} + #[tokio::test(flavor = "multi_thread")] async fn get_with_query() { run_wasi(test_programs_artifacts::CLIENT_GET_WITH_QUERY_COMPONENT)