Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ target/
tmp/
Cargo.lock
.DS_Store
*.swp
*.swo
7 changes: 7 additions & 0 deletions src/http/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ impl IntoBody for String {
}
}

impl IntoBody for Vec<u8> {
type IntoBody = BoundedBody<Vec<u8>>;
fn into_body(self) -> Self::IntoBody {
BoundedBody(Cursor::new(self))
}
}

impl<T> Body for T
where
T: AsyncRead,
Expand Down
9 changes: 3 additions & 6 deletions src/http/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ impl<'a> Client<'a> {

/// 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.try_into_outgoing()?;
let wasi_body = wasi_req.body().unwrap();
let body_stream = wasi_body.write().unwrap();

// 1. Start sending the request head
let res = wasi::http::outgoing_handler::handle(wasi_req, None).unwrap();

// 2. Start sending the request body
io::copy(body, OutputStream::new(&self.reactor, body_stream))
io::copy(body, OutputStream::new(self.reactor, body_stream))
.await
.expect("io::copy broke oh no");

Expand All @@ -41,10 +41,7 @@ impl<'a> Client<'a> {
// 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,
self.reactor.clone(),
)?)
Response::try_from_incoming_response(res, self.reactor.clone())
}
}

Expand Down
83 changes: 66 additions & 17 deletions src/http/fields.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{borrow::Cow, collections::HashMap, ops::Deref};
use wasi::http::types::{Fields as WasiFields, HeaderError};
use super::Error;
use std::{collections::HashMap, ops::Deref};
use wasi::http::types::{ErrorCode, Fields as WasiFields, HeaderError};

/// A type alias for [`Fields`] when used as HTTP headers.
pub type Headers = Fields;
Expand All @@ -8,27 +9,65 @@ pub type Headers = Fields;
pub type Trailers = Fields;

/// An HTTP Field name.
pub type FieldName = Cow<'static, str>;
pub type FieldName = String;

/// An HTTP Field value.
pub type FieldValue = Vec<u8>;

/// Field entry.
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct FieldEntry {
/// Field key in original case.
key: String,
/// Field values.
values: Vec<FieldValue>,
}

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

impl Fields {
pub fn get(&self, k: &FieldName) -> Option<&[FieldValue]> {
self.0.get(k).map(|f| f.deref())
pub(crate) fn new() -> Self {
Self(HashMap::new())
}
pub fn contains(&self, key: &str) -> bool {
self.0
.get(&key.to_lowercase())
.is_some_and(|entry| !entry.values.is_empty())
}
pub fn get(&self, key: &str) -> Option<&[FieldValue]> {
self.0
.get(&key.to_lowercase())
.map(|entry| entry.values.deref())
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut Vec<FieldValue>> {
self.0
.get_mut(&key.to_lowercase())
.map(|entry| entry.values.as_mut())
}
pub fn insert(&mut self, key: String, values: Vec<FieldValue>) {
self.0
.insert(key.to_lowercase(), FieldEntry { key, values });
}
pub fn append(&mut self, key: String, value: FieldValue) {
let entry: &mut FieldEntry = self.0.entry(key.to_lowercase()).or_insert(FieldEntry {
key,
values: Vec::with_capacity(1),
});
entry.values.push(value);
}
pub fn remove(&mut self, key: &str) -> Option<Vec<FieldValue>> {
self.0.remove(&key.to_lowercase()).map(|entry| entry.values)
}
}

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 {
let mut entries: Vec<_> = self.0.values().collect();
entries.sort_by_cached_key(|entry| entry.key.to_owned());
for FieldEntry { key, values } in entries {
match values.len() {
0 => {
map.entry(key, &"");
Expand All @@ -53,23 +92,33 @@ 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);
let field_name = key.to_lowercase();
let entry: &mut FieldEntry = output.entry(field_name).or_insert(FieldEntry {
key,
values: Vec::with_capacity(1),
});
entry.values.push(value);
}
Self(output)
}
}

impl TryFrom<Fields> for WasiFields {
type Error = HeaderError;
type Error = Error;
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() {
let mut list = Vec::with_capacity(fields.0.values().map(|entry| entry.values.len()).sum());
for FieldEntry { key, values } in fields.0.into_values() {
for value in values {
list.push((name.clone().into_owned(), value));
list.push((key.clone(), value));
}
}
Ok(WasiFields::from_list(&list)?)
WasiFields::from_list(&list).map_err(|e| {
let msg = match e {
HeaderError::InvalidSyntax => "header has invalid syntax",
HeaderError::Forbidden => "header key is forbidden",
HeaderError::Immutable => "headers are immutable",
};
ErrorCode::InternalError(Some(msg.to_string()))
})
}
}
21 changes: 14 additions & 7 deletions src/http/request.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use crate::io::{empty, Empty};

use super::{Body, IntoBody, Method};
use super::{Body, Headers, IntoBody, Method, Result};
use url::Url;
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<B: Body> {
method: Method,
url: Url,
headers: WasiHeaders,
headers: Headers,
body: B,
}

Expand All @@ -21,12 +21,19 @@ impl Request<Empty> {
body: empty(),
method,
url,
headers: WasiHeaders::new(),
headers: Headers::new(),
}
}
}

impl<B: Body> Request<B> {
pub fn headers(&self) -> &Headers {
&self.headers
}
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}

/// Set an HTTP body.
pub fn set_body<C: IntoBody>(self, body: C) -> Request<C::IntoBody> {
let Self {
Expand All @@ -43,8 +50,8 @@ impl<B: Body> Request<B> {
}
}

pub fn into_outgoing(self) -> (OutgoingRequest, B) {
let wasi_req = OutgoingRequest::new(self.headers);
pub fn try_into_outgoing(self) -> Result<(OutgoingRequest, B)> {
let wasi_req = OutgoingRequest::new(self.headers.try_into()?);

// Set the HTTP method
wasi_req.set_method(&self.method.into()).unwrap();
Expand All @@ -69,6 +76,6 @@ impl<B: Body> Request<B> {
wasi_req.set_authority(Some(self.url.authority())).unwrap();

// All done; request is ready for send-off
(wasi_req, self.body)
Ok((wasi_req, self.body))
}
}
Loading