Skip to content

Commit

Permalink
Merge pull request #22 from yoshuawuyts/pch/body_impls
Browse files Browse the repository at this point in the history
Refactor Body impls
  • Loading branch information
pchickey authored Nov 21, 2024
2 parents 283f11f + c58af7c commit 5ce367a
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 38 deletions.
15 changes: 13 additions & 2 deletions examples/http_get.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::error::Error;
use wstd::http::{Client, HeaderValue, Method, Request};
use wstd::http::{Body, Client, HeaderValue, Method, Request};
use wstd::io::AsyncRead;

#[wstd::main]
Expand All @@ -17,8 +17,19 @@ async fn main() -> Result<(), Box<dyn Error>> {
.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_len = body
.len()
.ok_or_else(|| "GET postman-echo.com/get is supposed to provide a content-length")?;

let mut body_buf = Vec::new();
let _body_len = response.body().read_to_end(&mut body_buf).await?;
body.read_to_end(&mut body_buf).await?;

assert_eq!(
body_buf.len(),
body_len,
"read_to_end length should match content-length"
);

let val: serde_json::Value = serde_json::from_slice(&body_buf)?;
let body_url = val
Expand Down
37 changes: 30 additions & 7 deletions src/http/body.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! HTTP body types
use crate::io::{AsyncRead, Cursor};
use crate::io::{AsyncRead, Cursor, Empty};

pub use super::response::IncomingBody;

Expand Down Expand Up @@ -41,12 +41,24 @@ impl IntoBody for String {
}
}

impl<T> Body for T
where
T: AsyncRead,
{
fn len(&self) -> Option<usize> {
None
impl IntoBody for &str {
type IntoBody = BoundedBody<Vec<u8>>;
fn into_body(self) -> Self::IntoBody {
BoundedBody(Cursor::new(self.to_owned().into_bytes()))
}
}

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

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

Expand All @@ -59,3 +71,14 @@ impl<T: AsRef<[u8]>> AsyncRead for BoundedBody<T> {
self.0.read(buf).await
}
}
impl<T: AsRef<[u8]>> Body for BoundedBody<T> {
fn len(&self) -> Option<usize> {
Some(self.0.get_ref().as_ref().len())
}
}

impl Body for Empty {
fn len(&self) -> Option<usize> {
Some(0)
}
}
72 changes: 43 additions & 29 deletions src/http/response.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use wasi::http::types::{IncomingBody as WasiIncomingBody, IncomingResponse};
use wasi::io::streams::{InputStream, StreamError};

use super::{fields::header_map_from_wasi, Body, HeaderMap, StatusCode};
use super::{fields::header_map_from_wasi, Body, Error, HeaderMap, Result, StatusCode};
use crate::io::AsyncRead;
use crate::runtime::Reactor;

Expand All @@ -16,39 +16,36 @@ pub struct Response<B: Body> {
body: B,
}

// #[derive(Debug)]
// enum BodyKind {
// Fixed(u64),
// Chunked,
// }

// impl BodyKind {
// fn from_headers(headers: &Fields) -> BodyKind {
// dbg!(&headers);
// if let Some(values) = headers.0.get("content-length") {
// let value = values
// .get(0)
// .expect("no value found for content-length; violates HTTP/1.1");
// let content_length = String::from_utf8(value.to_owned())
// .unwrap()
// .parse::<u64>()
// .expect("content-length should be a u64; violates HTTP/1.1");
// BodyKind::Fixed(content_length)
// } else if let Some(values) = headers.0.get("transfer-encoding") {
// dbg!(values);
// BodyKind::Chunked
// } else {
// dbg!("Encoding neither has a content-length nor transfer-encoding");
// BodyKind::Chunked
// }
// }
// }
#[derive(Debug)]
enum BodyKind {
Fixed(u64),
Chunked,
}

impl BodyKind {
fn from_headers(headers: &HeaderMap) -> Result<BodyKind> {
if let Some(value) = headers.get("content-length") {
let content_length = std::str::from_utf8(value.as_ref())
.unwrap()
.parse::<u64>()
.map_err(|_| {
Error::other("incoming content-length should be a u64; violates HTTP/1.1")
})?;
Ok(BodyKind::Fixed(content_length))
} else if headers.contains_key("transfer-encoding") {
Ok(BodyKind::Chunked)
} else {
Ok(BodyKind::Chunked)
}
}
}

impl Response<IncomingBody> {
pub(crate) fn try_from_incoming_response(incoming: IncomingResponse) -> super::Result<Self> {
pub(crate) fn try_from_incoming_response(incoming: IncomingResponse) -> Result<Self> {
let headers: HeaderMap = header_map_from_wasi(incoming.headers())?;
let status = incoming.status().into();

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
Expand All @@ -59,6 +56,7 @@ impl Response<IncomingBody> {
.expect("cannot call `stream` twice on an incoming body");

let body = IncomingBody {
kind,
buf_offset: 0,
buf: None,
body_stream,
Expand Down Expand Up @@ -97,6 +95,7 @@ impl<B: Body> Response<B> {
/// An incoming HTTP body
#[derive(Debug)]
pub struct IncomingBody {
kind: BodyKind,
buf: Option<Vec<u8>>,
// How many bytes have we already read from the buf?
buf_offset: usize,
Expand Down Expand Up @@ -147,3 +146,18 @@ impl AsyncRead for IncomingBody {
Ok(len)
}
}

impl Body for IncomingBody {
fn len(&self) -> Option<usize> {
match self.kind {
BodyKind::Fixed(l) => {
if l > (usize::MAX as u64) {
None
} else {
Some(l as usize)
}
}
BodyKind::Chunked => None,
}
}
}
54 changes: 54 additions & 0 deletions test-programs/src/bin/http_post.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::error::Error;
use wstd::http::{Client, HeaderValue, Method, Request};
use wstd::io::AsyncRead;

#[wstd::main]
async fn main() -> Result<(), Box<dyn Error>> {
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 mut response = Client::new()
.send(request.set_body("{\"test\": \"data\"}"))
.await?;

let content_type = response
.headers()
.get("Content-Type")
.ok_or_else(|| "response expected to have Content-Type header")?;
assert_eq!(content_type, "application/json; charset=utf-8");

let mut body_buf = Vec::new();
response.body().read_to_end(&mut body_buf).await?;

let val: serde_json::Value = serde_json::from_slice(&body_buf)?;
let body_url = val
.get("url")
.ok_or_else(|| "body json has url")?
.as_str()
.ok_or_else(|| "body json url is str")?;
assert!(
body_url.contains("postman-echo.com/post"),
"expected body url to contain the authority and path, got: {body_url}"
);

let posted_json = val
.get("json")
.ok_or_else(|| "body json has 'json' key")?
.as_object()
.ok_or_else(|| format!("body json 'json' is object. got {val:?}"))?;

assert_eq!(posted_json.len(), 1);
assert_eq!(
posted_json
.get("test")
.ok_or_else(|| "returned json has 'test' key")?
.as_str()
.ok_or_else(|| "returned json 'test' key should be str value")?,
"data"
);

Ok(())
}
7 changes: 7 additions & 0 deletions tests/test-programs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ fn http_get() -> Result<()> {
run_in_wasmtime(&wasm, None)
}

#[test]
fn http_post() -> Result<()> {
println!("testing {}", test_programs_artifacts::HTTP_POST);
let wasm = std::fs::read(test_programs_artifacts::HTTP_POST).context("read wasm")?;
run_in_wasmtime(&wasm, None)
}

#[test]
fn http_first_byte_timeout() -> Result<()> {
println!(
Expand Down

0 comments on commit 5ce367a

Please sign in to comment.