From c0b3ef82bf1d6805af5de0af8fd3faf33338649d Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 19 Nov 2024 12:58:45 -0800 Subject: [PATCH 1/6] swap out url::Url for http::uri::Uri same authors, slightly different interfaces --- Cargo.toml | 4 ++-- examples/http_get.rs | 4 ++-- src/http/mod.rs | 2 +- src/http/request.rs | 49 ++++++++++++++++++++++++-------------------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 33b9d39..f132298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,8 @@ authors = [ [features] [dependencies] +http.workspace = true slab.workspace = true -url.workspace = true wasi.workspace = true wstd-macro.workspace = true @@ -46,13 +46,13 @@ license = "MIT OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception" anyhow = "1" cargo_metadata = "0.18.1" heck = "0.5" +http = "1.1" quote = "1.0" serde_json = "1" slab = "0.4.9" syn = "2.0" test-programs = { path = "test-programs" } test-programs-artifacts = { path = "test-programs/artifacts" } -url = "2.5.0" wasi = "0.13.1" wasmtime = "26" wasmtime-wasi = "26" diff --git a/examples/http_get.rs b/examples/http_get.rs index c366e66..6809a1a 100644 --- a/examples/http_get.rs +++ b/examples/http_get.rs @@ -1,10 +1,10 @@ use std::error::Error; -use wstd::http::{Client, Method, Request, Url}; +use wstd::http::{Client, Method, Request}; use wstd::io::AsyncRead; #[wstd::main] async fn main() -> Result<(), Box> { - let request = Request::new(Method::Get, Url::parse("https://postman-echo.com/get")?); + let request = Request::new(Method::Get, "https://postman-echo.com/get".parse()?); let mut response = Client::new().send(request).await?; let content_type = response diff --git a/src/http/mod.rs b/src/http/mod.rs index a9d85de..a4dd56c 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,6 +1,6 @@ //! HTTP networking support -pub use url::Url; +pub use http::uri::Uri; #[doc(inline)] pub use body::{Body, IntoBody}; diff --git a/src/http/request.rs b/src/http/request.rs index f6bd58e..8c1725d 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,7 +1,7 @@ use crate::io::{empty, Empty}; use super::{Body, IntoBody, Method}; -use url::Url; +use http::uri::Uri; use wasi::http::outgoing_handler::OutgoingRequest; use wasi::http::types::{Headers as WasiHeaders, Scheme}; @@ -9,18 +9,18 @@ use wasi::http::types::{Headers as WasiHeaders, Scheme}; #[derive(Debug)] pub struct Request { method: Method, - url: Url, + uri: Uri, headers: WasiHeaders, body: B, } impl Request { /// Create a new HTTP request to send off to the client. - pub fn new(method: Method, url: Url) -> Self { + pub fn new(method: Method, uri: Uri) -> Self { Self { body: empty(), method, - url, + uri, headers: WasiHeaders::new(), } } @@ -31,42 +31,47 @@ impl Request { pub fn set_body(self, body: C) -> Request { let Self { method, - url, + uri, headers, .. } = self; Request { method, - url, + uri, headers, body: body.into_body(), } } - pub fn into_outgoing(self) -> (OutgoingRequest, B) { + pub(crate) fn into_outgoing(self) -> (OutgoingRequest, B) { let wasi_req = OutgoingRequest::new(self.headers); // Set the HTTP method - wasi_req.set_method(&self.method.into()).unwrap(); + wasi_req + .set_method(&self.method.into()) + .expect("method accepted by wasi-http implementation"); // Set the url scheme - let scheme = match self.url.scheme() { - "http" => Scheme::Http, - "https" => Scheme::Https, - other => Scheme::Other(other.to_owned()), + let scheme = match self.uri.scheme().map(|s| s.as_str()) { + Some("http") => Scheme::Http, + Some("https") | None => Scheme::Https, + Some(other) => Scheme::Other(other.to_owned()), }; - wasi_req.set_scheme(Some(&scheme)).unwrap(); + wasi_req + .set_scheme(Some(&scheme)) + .expect("scheme accepted by wasi-http implementation"); - // Set the url path + query string - let path = match self.url.query() { - Some(query) => format!("{}?{query}", self.url.path()), - None => self.url.path().to_owned(), - }; - wasi_req.set_path_with_query(Some(&path)).unwrap(); + // Set authority + wasi_req + .set_authority(self.uri.authority().map(|a| a.as_str())) + .expect("authority accepted by wasi-http implementation"); - // Not sure why we also have to set the authority, but sure we can do - // that too! - wasi_req.set_authority(Some(self.url.authority())).unwrap(); + // Set the url path + query string + if let Some(p_and_q) = self.uri.path_and_query() { + wasi_req + .set_path_with_query(Some(&p_and_q.to_string())) + .expect("path with query accepted by wasi-http implementation") + } // All done; request is ready for send-off (wasi_req, self.body) From 5e2ce1669f84dbc6839ea4ce3aee0ea5dcb194d2 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 19 Nov 2024 14:46:35 -0800 Subject: [PATCH 2/6] swap out local definitions of Fields, FieldName, FieldValue for http crate Closes #9 --- examples/http_get.rs | 7 ++-- src/http/error.rs | 88 +++++++++++++++++++++++++++++++++++++++++++- src/http/fields.rs | 87 ++++++++++--------------------------------- src/http/mod.rs | 4 +- src/http/response.rs | 4 +- 5 files changed, 114 insertions(+), 76 deletions(-) diff --git a/examples/http_get.rs b/examples/http_get.rs index 6809a1a..807a994 100644 --- a/examples/http_get.rs +++ b/examples/http_get.rs @@ -9,10 +9,9 @@ async fn main() -> Result<(), Box> { let content_type = response .headers() - .get(&"content-type".into()) - .ok_or_else(|| "response expected to have content-type header")?; - assert_eq!(content_type.len(), 1, "one header value for content-type"); - assert_eq!(content_type[0], b"application/json; charset=utf-8"); + .get("Content-Type") + .ok_or_else(|| "response expected to have Content-Type header")?; + assert_eq!(content_type, "application/json; charset=utf-8"); // Would much prefer read_to_end here: let mut body_buf = vec![0; 4096]; diff --git a/src/http/error.rs b/src/http/error.rs index 14f66b0..5a83d62 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -1,5 +1,89 @@ -/// The `http` error type. -pub type Error = wasi::http::types::ErrorCode; +use std::fmt; /// The `http` result type. pub type Result = std::result::Result; + +/// The `http` error type. +pub struct Error { + variant: ErrorVariant, + context: Vec, +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for c in self.context.iter() { + write!(f, "in {c}:\n")?; + } + match &self.variant { + ErrorVariant::WasiHttp(e) => write!(f, "wasi http error: {e:?}"), + ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e:?}"), + ErrorVariant::HeaderName(e) => write!(f, "header name error: {e:?}"), + ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e:?}"), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.variant { + ErrorVariant::WasiHttp(e) => write!(f, "wasi http error: {e}"), + ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e}"), + ErrorVariant::HeaderName(e) => write!(f, "header name error: {e}"), + ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e}"), + } + } +} + +impl std::error::Error for Error {} + +impl Error { + pub(crate) fn context(self, s: impl Into) -> Self { + let mut context = self.context; + context.push(s.into()); + Self { + variant: self.variant, + context, + } + } +} + +impl From for Error { + fn from(variant: ErrorVariant) -> Error { + Error { + variant, + context: Vec::new(), + } + } +} + +impl From for Error { + fn from(e: wasi::http::types::ErrorCode) -> Error { + ErrorVariant::WasiHttp(e).into() + } +} + +impl From for Error { + fn from(e: wasi::http::types::HeaderError) -> Error { + ErrorVariant::WasiHeader(e).into() + } +} + +impl From for Error { + fn from(e: http::header::InvalidHeaderValue) -> Error { + ErrorVariant::HeaderValue(e).into() + } +} + +impl From for Error { + fn from(e: http::header::InvalidHeaderName) -> Error { + ErrorVariant::HeaderName(e).into() + } +} + +#[derive(Debug)] +pub enum ErrorVariant { + WasiHttp(wasi::http::types::ErrorCode), + WasiHeader(wasi::http::types::HeaderError), + HeaderName(http::header::InvalidHeaderName), + HeaderValue(http::header::InvalidHeaderValue), +} diff --git a/src/http/fields.rs b/src/http/fields.rs index 71c1dc9..9f2408f 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,75 +1,28 @@ -use std::{borrow::Cow, collections::HashMap, ops::Deref}; -use wasi::http::types::{Fields as WasiFields, HeaderError}; - -/// A type alias for [`Fields`] when used as HTTP headers. +pub use http::header::{HeaderMap as Fields, HeaderName as FieldName, HeaderValue as FieldValue}; pub type Headers = Fields; - -/// A type alias for [`Fields`] when used as HTTP trailers. pub type Trailers = Fields; -/// An HTTP Field name. -pub type FieldName = Cow<'static, str>; - -/// An HTTP Field value. -pub type FieldValue = Vec; - -/// HTTP Fields which can be used as either trailers or headers. -#[derive(Clone, PartialEq, Eq)] -pub struct Fields(pub(crate) HashMap>); - -impl Fields { - pub fn get(&self, k: &FieldName) -> Option<&[FieldValue]> { - self.0.get(k).map(|f| f.deref()) - } -} - -impl std::fmt::Debug for Fields { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut map = f.debug_map(); - let mut entries: Vec<_> = self.0.iter().collect(); - entries.sort_by_cached_key(|(k, _)| k.to_owned()); - for (key, values) in entries { - match values.len() { - 0 => { - map.entry(key, &""); - } - 1 => { - let value = values.iter().next().unwrap(); - let value = String::from_utf8_lossy(value); - map.entry(key, &value); - } - _ => { - let values: Vec<_> = - values.iter().map(|v| String::from_utf8_lossy(v)).collect(); - map.entry(key, &values); - } - } - } - map.finish() - } -} - -impl From for Fields { - fn from(wasi_fields: WasiFields) -> Self { - let mut output = HashMap::new(); - for (key, value) in wasi_fields.entries() { - let field_name = key.into(); - let field_list: &mut Vec<_> = output.entry(field_name).or_default(); - field_list.push(value); - } - Self(output) +use super::{Error, Result}; +use wasi::http::types::Fields as WasiFields; + +pub(crate) fn fields_from_wasi(wasi_fields: WasiFields) -> Result { + let mut output = Fields::new(); + for (key, value) in wasi_fields.entries() { + let key = FieldName::from_bytes(key.as_bytes()) + .map_err(|e| Error::from(e).context("header name {key}"))?; + let value = FieldValue::from_bytes(&value) + .map_err(|e| Error::from(e).context("header value for {key}"))?; + output.append(key, value); } + Ok(output) } -impl TryFrom for WasiFields { - type Error = HeaderError; - fn try_from(fields: Fields) -> Result { - let mut list = Vec::with_capacity(fields.0.capacity()); - for (name, values) in fields.0.into_iter() { - for value in values { - list.push((name.clone().into_owned(), value)); - } - } - Ok(WasiFields::from_list(&list)?) +pub(crate) fn fields_to_wasi(fields: &Fields) -> Result { + let wasi_fields = WasiFields::new(); + for (key, value) in fields { + wasi_fields + .append(&key.as_str().to_owned(), &value.as_bytes().to_owned()) + .map_err(|e| Error::from(e).context("header named {key}"))?; } + Ok(wasi_fields) } diff --git a/src/http/mod.rs b/src/http/mod.rs index a4dd56c..88b27fd 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,5 +1,5 @@ //! HTTP networking support - +//! pub use http::uri::Uri; #[doc(inline)] @@ -12,6 +12,8 @@ pub use request::Request; pub use response::Response; pub use status_code::StatusCode; +pub(crate) use fields::fields_from_wasi; + pub mod body; mod client; diff --git a/src/http/response.rs b/src/http/response.rs index 8c38140..7313de8 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,7 +1,7 @@ use wasi::http::types::{IncomingBody as WasiIncomingBody, IncomingResponse}; use wasi::io::streams::{InputStream, StreamError}; -use super::{Body, Headers, StatusCode}; +use super::{fields_from_wasi, Body, Headers, StatusCode}; use crate::io::AsyncRead; use crate::runtime::Reactor; @@ -46,7 +46,7 @@ pub struct Response { impl Response { pub(crate) fn try_from_incoming_response(incoming: IncomingResponse) -> super::Result { - let headers: Headers = incoming.headers().into(); + let headers: Headers = fields_from_wasi(incoming.headers())?; let status = incoming.status().into(); // `body_stream` is a child of `incoming_body` which means we cannot From d28e4ca1eeccebef923f51b3a8a7a82095d1cabd Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 19 Nov 2024 15:06:40 -0800 Subject: [PATCH 3/6] re-export HeaderMap, HeaderName, HeaderValue under their names from http crate and get rid of the aliases. this crate doesnt even expose trailers, when we do it can just be a method called trailers(&self) -> &HeaderMap, no big deal. --- src/http/fields.rs | 21 ++++++++++----------- src/http/mod.rs | 4 ++-- src/http/response.rs | 10 +++++----- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/http/fields.rs b/src/http/fields.rs index 9f2408f..ab00bcc 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,25 +1,24 @@ -pub use http::header::{HeaderMap as Fields, HeaderName as FieldName, HeaderValue as FieldValue}; -pub type Headers = Fields; -pub type Trailers = Fields; +pub use http::header::{HeaderMap, HeaderName, HeaderValue}; use super::{Error, Result}; -use wasi::http::types::Fields as WasiFields; +use wasi::http::types::Fields; -pub(crate) fn fields_from_wasi(wasi_fields: WasiFields) -> Result { - let mut output = Fields::new(); +pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { + let mut output = HeaderMap::new(); for (key, value) in wasi_fields.entries() { - let key = FieldName::from_bytes(key.as_bytes()) + let key = HeaderName::from_bytes(key.as_bytes()) .map_err(|e| Error::from(e).context("header name {key}"))?; - let value = FieldValue::from_bytes(&value) + let value = HeaderValue::from_bytes(&value) .map_err(|e| Error::from(e).context("header value for {key}"))?; output.append(key, value); } Ok(output) } -pub(crate) fn fields_to_wasi(fields: &Fields) -> Result { - let wasi_fields = WasiFields::new(); - for (key, value) in fields { +#[allow(dead_code)] +pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { + let wasi_fields = Fields::new(); + for (key, value) in header_map { wasi_fields .append(&key.as_str().to_owned(), &value.as_bytes().to_owned()) .map_err(|e| Error::from(e).context("header named {key}"))?; diff --git a/src/http/mod.rs b/src/http/mod.rs index 88b27fd..30a78d2 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -6,13 +6,13 @@ pub use http::uri::Uri; pub use body::{Body, IntoBody}; pub use client::Client; pub use error::{Error, Result}; -pub use fields::{FieldName, FieldValue, Fields, Headers, Trailers}; +pub use fields::{HeaderMap, HeaderName, HeaderValue}; pub use method::Method; pub use request::Request; pub use response::Response; pub use status_code::StatusCode; -pub(crate) use fields::fields_from_wasi; +pub(crate) use fields::header_map_from_wasi; pub mod body; diff --git a/src/http/response.rs b/src/http/response.rs index 7313de8..dd8a7b3 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,7 +1,7 @@ use wasi::http::types::{IncomingBody as WasiIncomingBody, IncomingResponse}; use wasi::io::streams::{InputStream, StreamError}; -use super::{fields_from_wasi, Body, Headers, StatusCode}; +use super::{header_map_from_wasi, Body, HeaderMap, StatusCode}; use crate::io::AsyncRead; use crate::runtime::Reactor; @@ -11,7 +11,7 @@ const CHUNK_SIZE: u64 = 2048; /// An HTTP response #[derive(Debug)] pub struct Response { - headers: Headers, + headers: HeaderMap, status: StatusCode, body: B, } @@ -46,7 +46,7 @@ pub struct Response { impl Response { pub(crate) fn try_from_incoming_response(incoming: IncomingResponse) -> super::Result { - let headers: Headers = fields_from_wasi(incoming.headers())?; + let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; let status = incoming.status().into(); // `body_stream` is a child of `incoming_body` which means we cannot @@ -80,12 +80,12 @@ impl Response { } /// Get the HTTP headers from the impl - pub fn headers(&self) -> &Headers { + pub fn headers(&self) -> &HeaderMap { &self.headers } /// Mutably get the HTTP headers from the impl - pub fn headers_mut(&mut self) -> &mut Headers { + pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.headers } From 3d80fbc6015fcf554f9c7c680e6497289e9cb793 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 19 Nov 2024 15:19:04 -0800 Subject: [PATCH 4/6] add headers to requests and propogate some errors out instead of unwrapping --- src/http/client.rs | 2 +- src/http/error.rs | 6 ++++++ src/http/mod.rs | 2 +- src/http/request.rs | 40 +++++++++++++++++++++++++++------------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/http/client.rs b/src/http/client.rs index c32209f..02f932b 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -18,7 +18,7 @@ impl Client { /// Send an HTTP request. pub async fn send(&self, req: Request) -> Result> { - let (wasi_req, body) = req.into_outgoing(); + let (wasi_req, body) = req.into_outgoing()?; let wasi_body = wasi_req.body().unwrap(); let body_stream = wasi_body.write().unwrap(); diff --git a/src/http/error.rs b/src/http/error.rs index 5a83d62..2fabd2e 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -19,6 +19,7 @@ impl fmt::Debug for Error { ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e:?}"), ErrorVariant::HeaderName(e) => write!(f, "header name error: {e:?}"), ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e:?}"), + ErrorVariant::Other(e) => write!(f, "{e}"), } } } @@ -30,6 +31,7 @@ impl fmt::Display for Error { ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e}"), ErrorVariant::HeaderName(e) => write!(f, "header name error: {e}"), ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e}"), + ErrorVariant::Other(e) => write!(f, "{e}"), } } } @@ -37,6 +39,9 @@ impl fmt::Display for Error { impl std::error::Error for Error {} impl Error { + pub(crate) fn other(s: impl Into) -> Self { + ErrorVariant::Other(s.into()).into() + } pub(crate) fn context(self, s: impl Into) -> Self { let mut context = self.context; context.push(s.into()); @@ -86,4 +91,5 @@ pub enum ErrorVariant { WasiHeader(wasi::http::types::HeaderError), HeaderName(http::header::InvalidHeaderName), HeaderValue(http::header::InvalidHeaderValue), + Other(String), } diff --git a/src/http/mod.rs b/src/http/mod.rs index 30a78d2..2647370 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -12,7 +12,7 @@ pub use request::Request; pub use response::Response; pub use status_code::StatusCode; -pub(crate) use fields::header_map_from_wasi; +pub(crate) use fields::{header_map_from_wasi, header_map_to_wasi}; pub mod body; diff --git a/src/http/request.rs b/src/http/request.rs index 8c1725d..35ad65f 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,16 +1,16 @@ use crate::io::{empty, Empty}; -use super::{Body, IntoBody, Method}; +use super::{header_map_to_wasi, Body, Error, HeaderMap, IntoBody, Method, Result}; use http::uri::Uri; use wasi::http::outgoing_handler::OutgoingRequest; -use wasi::http::types::{Headers as WasiHeaders, Scheme}; +use wasi::http::types::Scheme; /// An HTTP request #[derive(Debug)] pub struct Request { method: Method, uri: Uri, - headers: WasiHeaders, + headers: HeaderMap, body: B, } @@ -21,12 +21,22 @@ impl Request { body: empty(), method, uri, - headers: WasiHeaders::new(), + headers: HeaderMap::new(), } } } impl Request { + /// Get the HTTP headers from the impl + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Mutably get the HTTP headers from the impl + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + /// Set an HTTP body. pub fn set_body(self, body: C) -> Request { let Self { @@ -43,13 +53,14 @@ impl Request { } } - pub(crate) fn into_outgoing(self) -> (OutgoingRequest, B) { - let wasi_req = OutgoingRequest::new(self.headers); + pub(crate) fn into_outgoing(self) -> Result<(OutgoingRequest, B)> { + let wasi_req = OutgoingRequest::new(header_map_to_wasi(&self.headers)?); // Set the HTTP method + let method = self.method.into(); wasi_req - .set_method(&self.method.into()) - .expect("method accepted by wasi-http implementation"); + .set_method(&method) + .map_err(|()| Error::other(format!("method rejected by wasi-http: {method:?}",)))?; // Set the url scheme let scheme = match self.uri.scheme().map(|s| s.as_str()) { @@ -59,21 +70,24 @@ impl Request { }; wasi_req .set_scheme(Some(&scheme)) - .expect("scheme accepted by wasi-http implementation"); + .map_err(|()| Error::other(format!("scheme rejected by wasi-http: {scheme:?}")))?; // Set authority + let authority = self.uri.authority().map(|a| a.as_str()); wasi_req - .set_authority(self.uri.authority().map(|a| a.as_str())) - .expect("authority accepted by wasi-http implementation"); + .set_authority(authority) + .map_err(|()| Error::other(format!("authority rejected by wasi-http {authority:?}")))?; // Set the url path + query string if let Some(p_and_q) = self.uri.path_and_query() { wasi_req .set_path_with_query(Some(&p_and_q.to_string())) - .expect("path with query accepted by wasi-http implementation") + .map_err(|()| { + Error::other(format!("path and query rejected by wasi-http {p_and_q:?}")) + })?; } // All done; request is ready for send-off - (wasi_req, self.body) + Ok((wasi_req, self.body)) } } From e51745fae3c484661336dedad2a20bd8f826518b Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 19 Nov 2024 15:36:30 -0800 Subject: [PATCH 5/6] also swap out local definition of Method for http's --- examples/http_get.rs | 2 +- src/http/error.rs | 9 +++++ src/http/fields.rs | 1 - src/http/method.rs | 93 ++++++++++++++------------------------------ src/http/mod.rs | 2 - src/http/request.rs | 7 +++- src/http/response.rs | 2 +- 7 files changed, 45 insertions(+), 71 deletions(-) diff --git a/examples/http_get.rs b/examples/http_get.rs index 807a994..eebec9f 100644 --- a/examples/http_get.rs +++ b/examples/http_get.rs @@ -4,7 +4,7 @@ use wstd::io::AsyncRead; #[wstd::main] async fn main() -> Result<(), Box> { - let request = Request::new(Method::Get, "https://postman-echo.com/get".parse()?); + let request = Request::new(Method::GET, "https://postman-echo.com/get".parse()?); let mut response = Client::new().send(request).await?; let content_type = response diff --git a/src/http/error.rs b/src/http/error.rs index 2fabd2e..4e50a22 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -19,6 +19,7 @@ impl fmt::Debug for Error { ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e:?}"), ErrorVariant::HeaderName(e) => write!(f, "header name error: {e:?}"), ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e:?}"), + ErrorVariant::Method(e) => write!(f, "method error: {e:?}"), ErrorVariant::Other(e) => write!(f, "{e}"), } } @@ -31,6 +32,7 @@ impl fmt::Display for Error { ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e}"), ErrorVariant::HeaderName(e) => write!(f, "header name error: {e}"), ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e}"), + ErrorVariant::Method(e) => write!(f, "method error: {e}"), ErrorVariant::Other(e) => write!(f, "{e}"), } } @@ -85,11 +87,18 @@ impl From for Error { } } +impl From for Error { + fn from(e: http::method::InvalidMethod) -> Error { + ErrorVariant::Method(e).into() + } +} + #[derive(Debug)] pub enum ErrorVariant { WasiHttp(wasi::http::types::ErrorCode), WasiHeader(wasi::http::types::HeaderError), HeaderName(http::header::InvalidHeaderName), HeaderValue(http::header::InvalidHeaderValue), + Method(http::method::InvalidMethod), Other(String), } diff --git a/src/http/fields.rs b/src/http/fields.rs index ab00bcc..22f7093 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -15,7 +15,6 @@ pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { Ok(output) } -#[allow(dead_code)] pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { let wasi_fields = Fields::new(); for (key, value) in header_map { diff --git a/src/http/method.rs b/src/http/method.rs index 7ef1e18..bd7c210 100644 --- a/src/http/method.rs +++ b/src/http/method.rs @@ -1,71 +1,36 @@ use wasi::http::types::Method as WasiMethod; -/// The method for the HTTP request -#[derive(Debug)] -#[non_exhaustive] -pub enum Method { - /// The GET method requests transfer of a current selected representation - /// for the target resource. - Get, - /// The HEAD method is identical to GET except that the server MUST NOT send a message body in - /// the response. - Head, - /// The POST method requests that the target resource process the representation enclosed in - /// the request according to the resource's own specific semantics. - Post, - /// The PUT method requests that the state of the target resource be created or replaced with - /// the state defined by the representation enclosed in the request message payload. - Put, - /// The DELETE method requests that the origin server remove the association between the target - /// resource and its current functionality. - Delete, - /// The CONNECT method requests that the recipient establish a tunnel to the destination origin - /// server identified by the request-target and, if successful, thereafter restrict its - /// behavior to blind forwarding of packets, in both directions, until the tunnel is closed. - Connect, - /// The OPTIONS method requests information about the communication options available for the - /// target resource, at either the origin server or an intervening intermediary. - Options, - /// The TRACE method requests a remote, application-level loop-back of the request message. - Trace, - /// The PATCH method requests that a set of changes described in the request entity be applied - /// to the resource identified by the Request- URI. - /// - Patch, - /// Send a method not covered by this list. - Other(String), -} +use super::Result; +pub use http::Method; -impl From for WasiMethod { - fn from(value: Method) -> Self { - match value { - Method::Get => WasiMethod::Get, - Method::Head => WasiMethod::Head, - Method::Post => WasiMethod::Post, - Method::Put => WasiMethod::Put, - Method::Delete => WasiMethod::Delete, - Method::Connect => WasiMethod::Connect, - Method::Options => WasiMethod::Options, - Method::Trace => WasiMethod::Trace, - Method::Patch => WasiMethod::Patch, - Method::Other(s) => WasiMethod::Other(s), - } +pub(crate) fn to_wasi_method(value: Method) -> WasiMethod { + match value { + Method::GET => WasiMethod::Get, + Method::HEAD => WasiMethod::Head, + Method::POST => WasiMethod::Post, + Method::PUT => WasiMethod::Put, + Method::DELETE => WasiMethod::Delete, + Method::CONNECT => WasiMethod::Connect, + Method::OPTIONS => WasiMethod::Options, + Method::TRACE => WasiMethod::Trace, + Method::PATCH => WasiMethod::Patch, + other => WasiMethod::Other(other.as_str().to_owned()), } } -impl From for Method { - fn from(value: WasiMethod) -> Self { - match value { - WasiMethod::Get => Method::Get, - WasiMethod::Head => Method::Head, - WasiMethod::Post => Method::Post, - WasiMethod::Put => Method::Put, - WasiMethod::Delete => Method::Delete, - WasiMethod::Connect => Method::Connect, - WasiMethod::Options => Method::Options, - WasiMethod::Trace => Method::Trace, - WasiMethod::Patch => Method::Patch, - WasiMethod::Other(s) => Method::Other(s), - } - } +// This will become useful once we support IncomingRequest +#[allow(dead_code)] +pub(crate) fn from_wasi_method(value: WasiMethod) -> Result { + Ok(match value { + WasiMethod::Get => Method::GET, + WasiMethod::Head => Method::HEAD, + WasiMethod::Post => Method::POST, + WasiMethod::Put => Method::PUT, + WasiMethod::Delete => Method::DELETE, + WasiMethod::Connect => Method::CONNECT, + WasiMethod::Options => Method::OPTIONS, + WasiMethod::Trace => Method::TRACE, + WasiMethod::Patch => Method::PATCH, + WasiMethod::Other(s) => Method::from_bytes(s.as_bytes())?, + }) } diff --git a/src/http/mod.rs b/src/http/mod.rs index 2647370..1de38b2 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -12,8 +12,6 @@ pub use request::Request; pub use response::Response; pub use status_code::StatusCode; -pub(crate) use fields::{header_map_from_wasi, header_map_to_wasi}; - pub mod body; mod client; diff --git a/src/http/request.rs b/src/http/request.rs index 35ad65f..65c469d 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,6 +1,9 @@ use crate::io::{empty, Empty}; -use super::{header_map_to_wasi, Body, Error, HeaderMap, IntoBody, Method, Result}; +use super::{ + fields::header_map_to_wasi, method::to_wasi_method, Body, Error, HeaderMap, IntoBody, Method, + Result, +}; use http::uri::Uri; use wasi::http::outgoing_handler::OutgoingRequest; use wasi::http::types::Scheme; @@ -57,7 +60,7 @@ impl Request { let wasi_req = OutgoingRequest::new(header_map_to_wasi(&self.headers)?); // Set the HTTP method - let method = self.method.into(); + let method = to_wasi_method(self.method); wasi_req .set_method(&method) .map_err(|()| Error::other(format!("method rejected by wasi-http: {method:?}",)))?; diff --git a/src/http/response.rs b/src/http/response.rs index dd8a7b3..dec56b1 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,7 +1,7 @@ use wasi::http::types::{IncomingBody as WasiIncomingBody, IncomingResponse}; use wasi::io::streams::{InputStream, StreamError}; -use super::{header_map_from_wasi, Body, HeaderMap, StatusCode}; +use super::{fields::header_map_from_wasi, Body, HeaderMap, StatusCode}; use crate::io::AsyncRead; use crate::runtime::Reactor; From 284d9ee246ce9571c6e2586d51c7765e5cd49026 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 19 Nov 2024 15:45:37 -0800 Subject: [PATCH 6/6] http_get test shows request headers work --- examples/http_get.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/examples/http_get.rs b/examples/http_get.rs index eebec9f..a54274c 100644 --- a/examples/http_get.rs +++ b/examples/http_get.rs @@ -1,10 +1,14 @@ use std::error::Error; -use wstd::http::{Client, Method, Request}; +use wstd::http::{Client, HeaderValue, Method, Request}; use wstd::io::AsyncRead; #[wstd::main] async fn main() -> Result<(), Box> { - let request = Request::new(Method::GET, "https://postman-echo.com/get".parse()?); + let mut request = Request::new(Method::GET, "https://postman-echo.com/get".parse()?); + request + .headers_mut() + .insert("my-header", HeaderValue::from_str("my-value")?); + let mut response = Client::new().send(request).await?; let content_type = response @@ -29,5 +33,15 @@ async fn main() -> Result<(), Box> { "expected body url to contain the authority and path, got: {body_url}" ); + assert_eq!( + val.get("headers") + .ok_or_else(|| "body json has headers")? + .get("my-header") + .ok_or_else(|| "headers contains my-header")? + .as_str() + .ok_or_else(|| "my-header is a str")?, + "my-value" + ); + Ok(()) }