diff --git a/src/http/body.rs b/src/http/body.rs index 27da3b4..4f50471 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -10,7 +10,7 @@ pub trait Body: AsyncRead { /// Returns the exact remaining length of the iterator, if known. fn len(&self) -> Option; - /// Returns `true`` if the body is known to be empty. + /// Returns `true` if the body is known to be empty. fn is_empty(&self) -> bool { matches!(self.len(), Some(0)) } diff --git a/src/http/client.rs b/src/http/client.rs index d064765..2e80b42 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -1,4 +1,6 @@ -use super::{response::IncomingBody, Body, Error, Request, Response, Result}; +use super::{body::IncomingBody, Body, Error, Request, Response, Result}; +use crate::http::request::into_outgoing; +use crate::http::response::try_from_incoming_response; use crate::io::{self, AsyncOutputStream, AsyncPollable}; use crate::time::Duration; use wasi::http::types::{OutgoingBody, RequestOptions as WasiRequestOptions}; @@ -18,7 +20,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) = into_outgoing(req)?; let wasi_body = wasi_req.body().unwrap(); let body_stream = wasi_body.write().unwrap(); @@ -39,7 +41,7 @@ impl Client { // is to trap if we try and get the response more than once. The final // `?` is to raise the actual error if there is one. let res = res.get().unwrap().unwrap()?; - Ok(Response::try_from_incoming_response(res)?) + try_from_incoming_response(res) } /// Set timeout on connecting to HTTP server diff --git a/src/http/mod.rs b/src/http/mod.rs index 269b446..1bc1aa3 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,5 +1,6 @@ //! HTTP networking support //! +pub use http::status::StatusCode; pub use http::uri::Uri; #[doc(inline)] @@ -10,7 +11,6 @@ pub use fields::{HeaderMap, HeaderName, HeaderValue}; pub use method::Method; pub use request::Request; pub use response::Response; -pub use status_code::StatusCode; pub mod body; @@ -20,4 +20,3 @@ mod fields; mod method; mod request; mod response; -mod status_code; diff --git a/src/http/request.rs b/src/http/request.rs index 65c469d..5ff1c8b 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,96 +1,45 @@ -use crate::io::{empty, Empty}; - -use super::{ - fields::header_map_to_wasi, method::to_wasi_method, Body, Error, HeaderMap, IntoBody, Method, - Result, -}; -use http::uri::Uri; +use super::{fields::header_map_to_wasi, method::to_wasi_method, Error, Result}; use wasi::http::outgoing_handler::OutgoingRequest; use wasi::http::types::Scheme; -/// An HTTP request -#[derive(Debug)] -pub struct Request { - method: Method, - uri: Uri, - headers: HeaderMap, - body: B, -} - -impl Request { - /// Create a new HTTP request to send off to the client. - pub fn new(method: Method, uri: Uri) -> Self { - Self { - body: empty(), - method, - uri, - 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 { - method, - uri, - headers, - .. - } = self; - Request { - method, - uri, - headers, - body: body.into_body(), - } - } - - 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 = to_wasi_method(self.method); - wasi_req - .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()) { - Some("http") => Scheme::Http, - Some("https") | None => Scheme::Https, - Some(other) => Scheme::Other(other.to_owned()), - }; +pub use http::Request; + +pub(crate) fn into_outgoing(request: Request) -> Result<(OutgoingRequest, T)> { + let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); + + let (parts, body) = request.into_parts(); + + // Set the HTTP method + let method = to_wasi_method(parts.method); + wasi_req + .set_method(&method) + .map_err(|()| Error::other(format!("method rejected by wasi-http: {method:?}",)))?; + + // Set the url scheme + let scheme = match parts.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)) + .map_err(|()| Error::other(format!("scheme rejected by wasi-http: {scheme:?}")))?; + + // Set authority + let authority = parts.uri.authority().map(|a| a.as_str()); + wasi_req + .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) = parts.uri.path_and_query() { wasi_req - .set_scheme(Some(&scheme)) - .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(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())) - .map_err(|()| { - Error::other(format!("path and query rejected by wasi-http {p_and_q:?}")) - })?; - } - - // All done; request is ready for send-off - Ok((wasi_req, self.body)) + .set_path_with_query(Some(&p_and_q.to_string())) + .map_err(|()| { + Error::other(format!("path and query rejected by wasi-http {p_and_q:?}")) + })?; } + + // All done; request is ready for send-off + Ok((wasi_req, body)) } diff --git a/src/http/response.rs b/src/http/response.rs index 463cc03..681b3bc 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,15 +1,10 @@ use wasi::http::types::{IncomingBody as WasiIncomingBody, IncomingResponse}; -use super::{fields::header_map_from_wasi, Body, Error, HeaderMap, Result, StatusCode}; +use super::{fields::header_map_from_wasi, Body, Error, HeaderMap, Result}; use crate::io::{AsyncInputStream, AsyncRead}; +use http::StatusCode; -/// An HTTP response -#[derive(Debug)] -pub struct Response { - headers: HeaderMap, - status: StatusCode, - body: B, -} +pub use http::Response; #[derive(Debug)] enum BodyKind { @@ -35,54 +30,39 @@ impl BodyKind { } } -impl Response { - pub(crate) fn try_from_incoming_response(incoming: IncomingResponse) -> Result { - let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; - let status = incoming.status().into(); +pub(crate) fn try_from_incoming_response( + incoming: IncomingResponse, +) -> Result> { + let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; + // TODO: Does WASI guarantee that the incoming status is valid? + let status = + StatusCode::from_u16(incoming.status()).map_err(|err| Error::other(err.to_string()))?; - let kind = BodyKind::from_headers(&headers)?; - // `body_stream` is a child of `incoming_body` which means we cannot - // drop the parent before we drop the child - let incoming_body = incoming - .consume() - .expect("cannot call `consume` twice on incoming response"); - let body_stream = incoming_body - .stream() - .expect("cannot call `stream` twice on an incoming body"); + let kind = BodyKind::from_headers(&headers)?; + // `body_stream` is a child of `incoming_body` which means we cannot + // drop the parent before we drop the child + let incoming_body = incoming + .consume() + .expect("cannot call `consume` twice on incoming response"); + let body_stream = incoming_body + .stream() + .expect("cannot call `stream` twice on an incoming body"); - let body = IncomingBody { - kind, - body_stream: AsyncInputStream::new(body_stream), - _incoming_body: incoming_body, - }; + let body = IncomingBody { + kind, + body_stream: AsyncInputStream::new(body_stream), + _incoming_body: incoming_body, + }; - Ok(Self { - headers, - body, - status, - }) - } -} + let mut builder = Response::builder().status(status); -impl Response { - // Get the HTTP status code - pub fn status_code(&self) -> StatusCode { - self.status + if let Some(headers_mut) = builder.headers_mut() { + *headers_mut = headers; } - /// 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 - } - - pub fn body(&mut self) -> &mut B { - &mut self.body - } + builder + .body(body) + .map_err(|err| Error::other(err.to_string())) } /// An incoming HTTP body diff --git a/src/http/status_code.rs b/src/http/status_code.rs deleted file mode 100644 index ad66b84..0000000 --- a/src/http/status_code.rs +++ /dev/null @@ -1,214 +0,0 @@ -use std::fmt::Display; - -/// HTTP Status Codes -/// -/// See the [Status Code -/// Registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml) -/// for more information -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u16)] -#[allow(missing_docs)] -#[non_exhaustive] -pub enum StatusCode { - Continue = 100, - SwitchingProtocols = 101, - Ok = 200, - Created = 201, - Accepted = 202, - NonAuthoritativeInformation = 203, - NoContent = 204, - ResetContent = 205, - PartialContent = 206, - MultipleChoices = 300, - MovedPermanently = 301, - Found = 302, - SeeOther = 303, - NotModified = 304, - UseProxy = 305, - TemporaryRedirect = 307, - PermanentRedirect = 308, - BadRequest = 400, - Unauthorized = 401, - PaymentRequired = 402, - Forbidden = 403, - NotFound = 404, - MethodNotAllowed = 405, - NotAcceptable = 406, - ProxyAuthenticationRequired = 407, - RequestTimeout = 408, - Conflict = 409, - Gone = 410, - LengthRequired = 411, - PreconditionFailed = 412, - ContentTooLarge = 413, - URITooLong = 414, - UnsupportedMediaType = 415, - RangeNotSatisfiable = 416, - ExpectationFailed = 417, - MisdirectedRequest = 421, - UnprocessableContent = 422, - UpgradeRequired = 426, - InternalServerError = 500, - NotImplemented = 501, - BadGateway = 502, - ServiceUnavailable = 503, - GatewayTimeout = 504, - HTTPVersionNotSupported = 505, - Other(u16), -} - -impl From for StatusCode { - fn from(input: u16) -> Self { - match input { - 100 => Self::Continue, - 101 => Self::SwitchingProtocols, - 200 => Self::Ok, - 201 => Self::Created, - 202 => Self::Accepted, - 203 => Self::NonAuthoritativeInformation, - 204 => Self::NoContent, - 205 => Self::ResetContent, - 206 => Self::PartialContent, - 300 => Self::MultipleChoices, - 301 => Self::MovedPermanently, - 302 => Self::Found, - 303 => Self::SeeOther, - 304 => Self::NotModified, - 305 => Self::UseProxy, - 307 => Self::TemporaryRedirect, - 308 => Self::PermanentRedirect, - 400 => Self::BadRequest, - 401 => Self::Unauthorized, - 402 => Self::PaymentRequired, - 403 => Self::Forbidden, - 404 => Self::NotFound, - 405 => Self::MethodNotAllowed, - 406 => Self::NotAcceptable, - 407 => Self::ProxyAuthenticationRequired, - 408 => Self::RequestTimeout, - 409 => Self::Conflict, - 410 => Self::Gone, - 411 => Self::LengthRequired, - 412 => Self::PreconditionFailed, - 413 => Self::ContentTooLarge, - 414 => Self::URITooLong, - 415 => Self::UnsupportedMediaType, - 416 => Self::RangeNotSatisfiable, - 417 => Self::ExpectationFailed, - 421 => Self::MisdirectedRequest, - 422 => Self::UnprocessableContent, - 426 => Self::UpgradeRequired, - 500 => Self::InternalServerError, - 501 => Self::NotImplemented, - 502 => Self::BadGateway, - 503 => Self::ServiceUnavailable, - 504 => Self::GatewayTimeout, - 505 => Self::HTTPVersionNotSupported, - code => Self::Other(code), - } - } -} - -impl From for u16 { - fn from(input: StatusCode) -> Self { - match input { - StatusCode::Continue => 100, - StatusCode::SwitchingProtocols => 101, - StatusCode::Ok => 200, - StatusCode::Created => 201, - StatusCode::Accepted => 202, - StatusCode::NonAuthoritativeInformation => 203, - StatusCode::NoContent => 204, - StatusCode::ResetContent => 205, - StatusCode::PartialContent => 206, - StatusCode::MultipleChoices => 300, - StatusCode::MovedPermanently => 301, - StatusCode::Found => 302, - StatusCode::SeeOther => 303, - StatusCode::NotModified => 304, - StatusCode::UseProxy => 305, - StatusCode::TemporaryRedirect => 307, - StatusCode::PermanentRedirect => 308, - StatusCode::BadRequest => 400, - StatusCode::Unauthorized => 401, - StatusCode::PaymentRequired => 402, - StatusCode::Forbidden => 403, - StatusCode::NotFound => 404, - StatusCode::MethodNotAllowed => 405, - StatusCode::NotAcceptable => 406, - StatusCode::ProxyAuthenticationRequired => 407, - StatusCode::RequestTimeout => 408, - StatusCode::Conflict => 409, - StatusCode::Gone => 410, - StatusCode::LengthRequired => 411, - StatusCode::PreconditionFailed => 412, - StatusCode::ContentTooLarge => 413, - StatusCode::URITooLong => 414, - StatusCode::UnsupportedMediaType => 415, - StatusCode::RangeNotSatisfiable => 416, - StatusCode::ExpectationFailed => 417, - StatusCode::MisdirectedRequest => 421, - StatusCode::UnprocessableContent => 422, - StatusCode::UpgradeRequired => 426, - StatusCode::InternalServerError => 500, - StatusCode::NotImplemented => 501, - StatusCode::BadGateway => 502, - StatusCode::ServiceUnavailable => 503, - StatusCode::GatewayTimeout => 504, - StatusCode::HTTPVersionNotSupported => 505, - StatusCode::Other(status) => status, - } - } -} - -impl Display for StatusCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StatusCode::Continue => write!(f, "100 Continue"), - StatusCode::SwitchingProtocols => write!(f, "101 Switching Protocols"), - StatusCode::Ok => write!(f, "200 Ok"), - StatusCode::Created => write!(f, "201 Created"), - StatusCode::Accepted => write!(f, "202 Accepted"), - StatusCode::NonAuthoritativeInformation => write!(f, "203 NonAuthoritativeInformation"), - StatusCode::NoContent => write!(f, "204 NoContent"), - StatusCode::ResetContent => write!(f, "205 ResetContent"), - StatusCode::PartialContent => write!(f, "206 PartialContent"), - StatusCode::MultipleChoices => write!(f, "300 MultipleChoices"), - StatusCode::MovedPermanently => write!(f, "301 MovedPermanently"), - StatusCode::Found => write!(f, "302 Found"), - StatusCode::SeeOther => write!(f, "303 SeeOther"), - StatusCode::NotModified => write!(f, "304 NotModified"), - StatusCode::UseProxy => write!(f, "305 UseProxy"), - StatusCode::TemporaryRedirect => write!(f, "307 TemporaryRedirect"), - StatusCode::PermanentRedirect => write!(f, "308 PermanentRedirect"), - StatusCode::BadRequest => write!(f, "400 BadRequest"), - StatusCode::Unauthorized => write!(f, "401 Unauthorized"), - StatusCode::PaymentRequired => write!(f, "402 PaymentRequired"), - StatusCode::Forbidden => write!(f, "403 Forbidden"), - StatusCode::NotFound => write!(f, "404 NotFound"), - StatusCode::MethodNotAllowed => write!(f, "405 MethodNotAllowed"), - StatusCode::NotAcceptable => write!(f, "406 NotAcceptable"), - StatusCode::ProxyAuthenticationRequired => write!(f, "407 ProxyAuthenticationRequired"), - StatusCode::RequestTimeout => write!(f, "408 RequestTimeout"), - StatusCode::Conflict => write!(f, "409 Conflict"), - StatusCode::Gone => write!(f, "410 Gone"), - StatusCode::LengthRequired => write!(f, "411 LengthRequired"), - StatusCode::PreconditionFailed => write!(f, "412 PreconditionFailed"), - StatusCode::ContentTooLarge => write!(f, "413 ContentTooLarge"), - StatusCode::URITooLong => write!(f, "414 URITooLong"), - StatusCode::UnsupportedMediaType => write!(f, "415 UnsupportedMediaType"), - StatusCode::RangeNotSatisfiable => write!(f, "416 RangeNotSatisfiable"), - StatusCode::ExpectationFailed => write!(f, "417 ExpectationFailed"), - StatusCode::MisdirectedRequest => write!(f, "421 MisdirectedRequest"), - StatusCode::UnprocessableContent => write!(f, "422 UnprocessableContent"), - StatusCode::UpgradeRequired => write!(f, "426 UpgradeRequired"), - StatusCode::InternalServerError => write!(f, "500 InternalServerError"), - StatusCode::NotImplemented => write!(f, "501 NotImplemented"), - StatusCode::BadGateway => write!(f, "502 BadGateway"), - StatusCode::ServiceUnavailable => write!(f, "503 ServiceUnavailable"), - StatusCode::GatewayTimeout => write!(f, "504 GatewayTimeout"), - StatusCode::HTTPVersionNotSupported => write!(f, "505 HTTPVersionNotSupported"), - StatusCode::Other(status) => write!(f, "{status}"), - } - } -} diff --git a/tests/http_first_byte_timeout.rs b/tests/http_first_byte_timeout.rs index c598e3e..f8a0ac3 100644 --- a/tests/http_first_byte_timeout.rs +++ b/tests/http_first_byte_timeout.rs @@ -1,7 +1,8 @@ use wstd::http::{ error::{ErrorVariant, WasiHttpErrorCode}, - Client, Method, Request, + Client, Request, }; +use wstd::io::empty; #[wstd::main] async fn main() -> Result<(), Box> { @@ -10,7 +11,7 @@ async fn main() -> Result<(), Box> { client.set_first_byte_timeout(std::time::Duration::from_millis(500)); // This get request will connect to the server, which will then wait 1 second before // returning a response. - let request = Request::new(Method::GET, "https://postman-echo.com/delay/1".parse()?); + let request = Request::get("https://postman-echo.com/delay/1").body(empty())?; let result = client.send(request).await; assert!(result.is_err(), "response should be an error"); diff --git a/tests/http_get.rs b/tests/http_get.rs index 9603736..9100237 100644 --- a/tests/http_get.rs +++ b/tests/http_get.rs @@ -1,13 +1,12 @@ use std::error::Error; -use wstd::http::{Body, Client, HeaderValue, Method, Request}; -use wstd::io::AsyncRead; +use wstd::http::{Body, Client, HeaderValue, Request}; +use wstd::io::{empty, AsyncRead}; #[wstd::test] async fn main() -> Result<(), Box> { - 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 request = Request::get("https://postman-echo.com/get") + .header("my-header", HeaderValue::from_str("my-value")?) + .body(empty())?; let mut response = Client::new().send(request).await?; @@ -17,7 +16,7 @@ async fn main() -> Result<(), Box> { .ok_or_else(|| "response expected to have Content-Type header")?; assert_eq!(content_type, "application/json; charset=utf-8"); - let body = response.body(); + let body = response.body_mut(); let body_len = body .len() .ok_or_else(|| "GET postman-echo.com/get is supposed to provide a content-length")?; diff --git a/tests/http_post.rs b/tests/http_post.rs index fc23339..c2257a4 100644 --- a/tests/http_post.rs +++ b/tests/http_post.rs @@ -1,18 +1,17 @@ use std::error::Error; -use wstd::http::{Client, HeaderValue, Method, Request}; +use wstd::http::{Client, HeaderValue, IntoBody, Request}; use wstd::io::AsyncRead; #[wstd::test] async fn main() -> Result<(), Box> { - let mut request = Request::new(Method::POST, "https://postman-echo.com/post".parse()?); - request.headers_mut().insert( - "content-type", - HeaderValue::from_str("application/json; charset=utf-8")?, - ); + let request = Request::post("https://postman-echo.com/post") + .header( + "content-type", + HeaderValue::from_str("application/json; charset=utf-8")?, + ) + .body("{\"test\": \"data\"}".into_body())?; - let mut response = Client::new() - .send(request.set_body("{\"test\": \"data\"}")) - .await?; + let mut response = Client::new().send(request).await?; let content_type = response .headers() @@ -21,7 +20,7 @@ async fn main() -> Result<(), Box> { assert_eq!(content_type, "application/json; charset=utf-8"); let mut body_buf = Vec::new(); - response.body().read_to_end(&mut body_buf).await?; + response.body_mut().read_to_end(&mut body_buf).await?; let val: serde_json::Value = serde_json::from_slice(&body_buf)?; let body_url = val diff --git a/tests/http_timeout.rs b/tests/http_timeout.rs index de67f1b..dea1ac9 100644 --- a/tests/http_timeout.rs +++ b/tests/http_timeout.rs @@ -1,12 +1,13 @@ use wstd::future::FutureExt; -use wstd::http::{Client, Method, Request}; +use wstd::http::{Client, Request}; +use wstd::io::empty; use wstd::time::Duration; #[wstd::test] async fn http_timeout() -> Result<(), Box> { // This get request will connect to the server, which will then wait 1 second before // returning a response. - let request = Request::new(Method::GET, "https://postman-echo.com/delay/1".parse()?); + let request = Request::get("https://postman-echo.com/delay/1").body(empty())?; let result = Client::new() .send(request) .timeout(Duration::from_millis(500))