Skip to content

Commit 5ce367a

Browse files
authored
Merge pull request #22 from yoshuawuyts/pch/body_impls
Refactor Body impls
2 parents 283f11f + c58af7c commit 5ce367a

File tree

5 files changed

+147
-38
lines changed

5 files changed

+147
-38
lines changed

examples/http_get.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::error::Error;
2-
use wstd::http::{Client, HeaderValue, Method, Request};
2+
use wstd::http::{Body, Client, HeaderValue, Method, Request};
33
use wstd::io::AsyncRead;
44

55
#[wstd::main]
@@ -17,8 +17,19 @@ async fn main() -> Result<(), Box<dyn Error>> {
1717
.ok_or_else(|| "response expected to have Content-Type header")?;
1818
assert_eq!(content_type, "application/json; charset=utf-8");
1919

20+
let body = response.body();
21+
let body_len = body
22+
.len()
23+
.ok_or_else(|| "GET postman-echo.com/get is supposed to provide a content-length")?;
24+
2025
let mut body_buf = Vec::new();
21-
let _body_len = response.body().read_to_end(&mut body_buf).await?;
26+
body.read_to_end(&mut body_buf).await?;
27+
28+
assert_eq!(
29+
body_buf.len(),
30+
body_len,
31+
"read_to_end length should match content-length"
32+
);
2233

2334
let val: serde_json::Value = serde_json::from_slice(&body_buf)?;
2435
let body_url = val

src/http/body.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! HTTP body types
22
3-
use crate::io::{AsyncRead, Cursor};
3+
use crate::io::{AsyncRead, Cursor, Empty};
44

55
pub use super::response::IncomingBody;
66

@@ -41,12 +41,24 @@ impl IntoBody for String {
4141
}
4242
}
4343

44-
impl<T> Body for T
45-
where
46-
T: AsyncRead,
47-
{
48-
fn len(&self) -> Option<usize> {
49-
None
44+
impl IntoBody for &str {
45+
type IntoBody = BoundedBody<Vec<u8>>;
46+
fn into_body(self) -> Self::IntoBody {
47+
BoundedBody(Cursor::new(self.to_owned().into_bytes()))
48+
}
49+
}
50+
51+
impl IntoBody for Vec<u8> {
52+
type IntoBody = BoundedBody<Vec<u8>>;
53+
fn into_body(self) -> Self::IntoBody {
54+
BoundedBody(Cursor::new(self))
55+
}
56+
}
57+
58+
impl IntoBody for &[u8] {
59+
type IntoBody = BoundedBody<Vec<u8>>;
60+
fn into_body(self) -> Self::IntoBody {
61+
BoundedBody(Cursor::new(self.to_owned()))
5062
}
5163
}
5264

@@ -59,3 +71,14 @@ impl<T: AsRef<[u8]>> AsyncRead for BoundedBody<T> {
5971
self.0.read(buf).await
6072
}
6173
}
74+
impl<T: AsRef<[u8]>> Body for BoundedBody<T> {
75+
fn len(&self) -> Option<usize> {
76+
Some(self.0.get_ref().as_ref().len())
77+
}
78+
}
79+
80+
impl Body for Empty {
81+
fn len(&self) -> Option<usize> {
82+
Some(0)
83+
}
84+
}

src/http/response.rs

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use wasi::http::types::{IncomingBody as WasiIncomingBody, IncomingResponse};
22
use wasi::io::streams::{InputStream, StreamError};
33

4-
use super::{fields::header_map_from_wasi, Body, HeaderMap, StatusCode};
4+
use super::{fields::header_map_from_wasi, Body, Error, HeaderMap, Result, StatusCode};
55
use crate::io::AsyncRead;
66
use crate::runtime::Reactor;
77

@@ -16,39 +16,36 @@ pub struct Response<B: Body> {
1616
body: B,
1717
}
1818

19-
// #[derive(Debug)]
20-
// enum BodyKind {
21-
// Fixed(u64),
22-
// Chunked,
23-
// }
24-
25-
// impl BodyKind {
26-
// fn from_headers(headers: &Fields) -> BodyKind {
27-
// dbg!(&headers);
28-
// if let Some(values) = headers.0.get("content-length") {
29-
// let value = values
30-
// .get(0)
31-
// .expect("no value found for content-length; violates HTTP/1.1");
32-
// let content_length = String::from_utf8(value.to_owned())
33-
// .unwrap()
34-
// .parse::<u64>()
35-
// .expect("content-length should be a u64; violates HTTP/1.1");
36-
// BodyKind::Fixed(content_length)
37-
// } else if let Some(values) = headers.0.get("transfer-encoding") {
38-
// dbg!(values);
39-
// BodyKind::Chunked
40-
// } else {
41-
// dbg!("Encoding neither has a content-length nor transfer-encoding");
42-
// BodyKind::Chunked
43-
// }
44-
// }
45-
// }
19+
#[derive(Debug)]
20+
enum BodyKind {
21+
Fixed(u64),
22+
Chunked,
23+
}
24+
25+
impl BodyKind {
26+
fn from_headers(headers: &HeaderMap) -> Result<BodyKind> {
27+
if let Some(value) = headers.get("content-length") {
28+
let content_length = std::str::from_utf8(value.as_ref())
29+
.unwrap()
30+
.parse::<u64>()
31+
.map_err(|_| {
32+
Error::other("incoming content-length should be a u64; violates HTTP/1.1")
33+
})?;
34+
Ok(BodyKind::Fixed(content_length))
35+
} else if headers.contains_key("transfer-encoding") {
36+
Ok(BodyKind::Chunked)
37+
} else {
38+
Ok(BodyKind::Chunked)
39+
}
40+
}
41+
}
4642

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

48+
let kind = BodyKind::from_headers(&headers)?;
5249
// `body_stream` is a child of `incoming_body` which means we cannot
5350
// drop the parent before we drop the child
5451
let incoming_body = incoming
@@ -59,6 +56,7 @@ impl Response<IncomingBody> {
5956
.expect("cannot call `stream` twice on an incoming body");
6057

6158
let body = IncomingBody {
59+
kind,
6260
buf_offset: 0,
6361
buf: None,
6462
body_stream,
@@ -97,6 +95,7 @@ impl<B: Body> Response<B> {
9795
/// An incoming HTTP body
9896
#[derive(Debug)]
9997
pub struct IncomingBody {
98+
kind: BodyKind,
10099
buf: Option<Vec<u8>>,
101100
// How many bytes have we already read from the buf?
102101
buf_offset: usize,
@@ -147,3 +146,18 @@ impl AsyncRead for IncomingBody {
147146
Ok(len)
148147
}
149148
}
149+
150+
impl Body for IncomingBody {
151+
fn len(&self) -> Option<usize> {
152+
match self.kind {
153+
BodyKind::Fixed(l) => {
154+
if l > (usize::MAX as u64) {
155+
None
156+
} else {
157+
Some(l as usize)
158+
}
159+
}
160+
BodyKind::Chunked => None,
161+
}
162+
}
163+
}

test-programs/src/bin/http_post.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use std::error::Error;
2+
use wstd::http::{Client, HeaderValue, Method, Request};
3+
use wstd::io::AsyncRead;
4+
5+
#[wstd::main]
6+
async fn main() -> Result<(), Box<dyn Error>> {
7+
let mut request = Request::new(Method::POST, "https://postman-echo.com/post".parse()?);
8+
request.headers_mut().insert(
9+
"content-type",
10+
HeaderValue::from_str("application/json; charset=utf-8")?,
11+
);
12+
13+
let mut response = Client::new()
14+
.send(request.set_body("{\"test\": \"data\"}"))
15+
.await?;
16+
17+
let content_type = response
18+
.headers()
19+
.get("Content-Type")
20+
.ok_or_else(|| "response expected to have Content-Type header")?;
21+
assert_eq!(content_type, "application/json; charset=utf-8");
22+
23+
let mut body_buf = Vec::new();
24+
response.body().read_to_end(&mut body_buf).await?;
25+
26+
let val: serde_json::Value = serde_json::from_slice(&body_buf)?;
27+
let body_url = val
28+
.get("url")
29+
.ok_or_else(|| "body json has url")?
30+
.as_str()
31+
.ok_or_else(|| "body json url is str")?;
32+
assert!(
33+
body_url.contains("postman-echo.com/post"),
34+
"expected body url to contain the authority and path, got: {body_url}"
35+
);
36+
37+
let posted_json = val
38+
.get("json")
39+
.ok_or_else(|| "body json has 'json' key")?
40+
.as_object()
41+
.ok_or_else(|| format!("body json 'json' is object. got {val:?}"))?;
42+
43+
assert_eq!(posted_json.len(), 1);
44+
assert_eq!(
45+
posted_json
46+
.get("test")
47+
.ok_or_else(|| "returned json has 'test' key")?
48+
.as_str()
49+
.ok_or_else(|| "returned json 'test' key should be str value")?,
50+
"data"
51+
);
52+
53+
Ok(())
54+
}

tests/test-programs.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ fn http_get() -> Result<()> {
127127
run_in_wasmtime(&wasm, None)
128128
}
129129

130+
#[test]
131+
fn http_post() -> Result<()> {
132+
println!("testing {}", test_programs_artifacts::HTTP_POST);
133+
let wasm = std::fs::read(test_programs_artifacts::HTTP_POST).context("read wasm")?;
134+
run_in_wasmtime(&wasm, None)
135+
}
136+
130137
#[test]
131138
fn http_first_byte_timeout() -> Result<()> {
132139
println!(

0 commit comments

Comments
 (0)