Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ authors = [
[features]

[dependencies]
http.workspace = true
slab.workspace = true
url.workspace = true
wasi.workspace = true
wstd-macro.workspace = true

Expand Down Expand Up @@ -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"
Expand Down
25 changes: 19 additions & 6 deletions examples/http_get.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
use std::error::Error;
use wstd::http::{Client, Method, Request, Url};
use wstd::http::{Client, HeaderValue, Method, Request};
use wstd::io::AsyncRead;

#[wstd::main]
async fn main() -> Result<(), Box<dyn Error>> {
let request = Request::new(Method::Get, Url::parse("https://postman-echo.com/get")?);
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
.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];
Expand All @@ -30,5 +33,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
"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(())
}
2 changes: 1 addition & 1 deletion src/http/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl Client {

/// Send an HTTP request.
pub async fn send<B: Body>(&self, req: Request<B>) -> Result<Response<IncomingBody>> {
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();

Expand Down
103 changes: 101 additions & 2 deletions src/http/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,104 @@
/// The `http` error type.
pub type Error = wasi::http::types::ErrorCode;
use std::fmt;

/// The `http` result type.
pub type Result<T> = std::result::Result<T, Error>;

/// The `http` error type.
pub struct Error {
variant: ErrorVariant,
context: Vec<String>,
}

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:?}"),
ErrorVariant::Method(e) => write!(f, "method error: {e:?}"),
ErrorVariant::Other(e) => write!(f, "{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}"),
ErrorVariant::Method(e) => write!(f, "method error: {e}"),
ErrorVariant::Other(e) => write!(f, "{e}"),
}
}
}

impl std::error::Error for Error {}

impl Error {
pub(crate) fn other(s: impl Into<String>) -> Self {
ErrorVariant::Other(s.into()).into()
}
pub(crate) fn context(self, s: impl Into<String>) -> Self {
let mut context = self.context;
context.push(s.into());
Self {
variant: self.variant,
context,
}
}
}

impl From<ErrorVariant> for Error {
fn from(variant: ErrorVariant) -> Error {
Error {
variant,
context: Vec::new(),
}
}
}

impl From<wasi::http::types::ErrorCode> for Error {
fn from(e: wasi::http::types::ErrorCode) -> Error {
ErrorVariant::WasiHttp(e).into()
}
}

impl From<wasi::http::types::HeaderError> for Error {
fn from(e: wasi::http::types::HeaderError) -> Error {
ErrorVariant::WasiHeader(e).into()
}
}

impl From<http::header::InvalidHeaderValue> for Error {
fn from(e: http::header::InvalidHeaderValue) -> Error {
ErrorVariant::HeaderValue(e).into()
}
}

impl From<http::header::InvalidHeaderName> for Error {
fn from(e: http::header::InvalidHeaderName) -> Error {
ErrorVariant::HeaderName(e).into()
}
}

impl From<http::method::InvalidMethod> 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),
}
91 changes: 21 additions & 70 deletions src/http/fields.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,26 @@
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 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<u8>;

/// HTTP Fields which can be used as either trailers or headers.
#[derive(Clone, PartialEq, Eq)]
pub struct Fields(pub(crate) HashMap<FieldName, Vec<FieldValue>>);

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<WasiFields> 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)
pub use http::header::{HeaderMap, HeaderName, HeaderValue};

use super::{Error, Result};
use wasi::http::types::Fields;

pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result<HeaderMap> {
let mut output = HeaderMap::new();
for (key, value) in wasi_fields.entries() {
let key = HeaderName::from_bytes(key.as_bytes())
.map_err(|e| Error::from(e).context("header name {key}"))?;
let value = HeaderValue::from_bytes(&value)
.map_err(|e| Error::from(e).context("header value for {key}"))?;
output.append(key, value);
}
Ok(output)
}

impl TryFrom<Fields> for WasiFields {
type Error = HeaderError;
fn try_from(fields: Fields) -> Result<Self, Self::Error> {
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 header_map_to_wasi(header_map: &HeaderMap) -> Result<Fields> {
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}"))?;
}
Ok(wasi_fields)
}
93 changes: 29 additions & 64 deletions src/http/method.rs
Original file line number Diff line number Diff line change
@@ -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<Method> 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<WasiMethod> 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<Method> {
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())?,
})
}
6 changes: 3 additions & 3 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! HTTP networking support

pub use url::Url;
//!
pub use http::uri::Uri;

#[doc(inline)]
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;
Expand Down
Loading
Loading