From c1451ba0f557506d5c96114e5ef197b688a96bbd Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Thu, 4 Sep 2025 16:42:21 -0700
Subject: [PATCH 01/10] implement bodies using http_body::Body
---
Cargo.toml | 13 +-
examples/complex_http_client.rs | 43 +-
examples/http_client.rs | 45 +-
examples/http_server.rs | 146 ++--
macro/src/lib.rs | 21 +-
src/http/body.rs | 699 +++++++++++--------
src/http/client.rs | 164 +----
src/http/error.rs | 129 +---
src/http/fields.rs | 25 +-
src/http/mod.rs | 4 +-
src/http/request.rs | 126 +---
src/http/response.rs | 31 +-
src/http/server.rs | 165 +----
src/io/cursor.rs | 4 +
src/io/empty.rs | 3 +
src/io/read.rs | 2 +
src/io/stdio.rs | 18 +
src/io/streams.rs | 19 +-
src/io/write.rs | 2 +
src/lib.rs | 1 -
src/net/tcp_stream.rs | 6 +
src/runtime/reactor.rs | 51 +-
test-programs/Cargo.toml | 2 +
test-programs/artifacts/build.rs | 6 +
test-programs/artifacts/tests/http_server.rs | 139 +++-
tests/http_first_byte_timeout.rs | 12 +-
tests/http_get.rs | 32 +-
tests/http_get_json.rs | 11 +-
tests/http_post.rs | 25 +-
tests/http_post_json.rs | 20 +-
tests/http_timeout.rs | 5 +-
31 files changed, 940 insertions(+), 1029 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index c237caf..0a3cce5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,8 +17,13 @@ default = ["json"]
json = ["dep:serde", "dep:serde_json"]
[dependencies]
+anyhow.workspace = true
async-task.workspace = true
-futures-core.workspace = true
+async-trait.workspace = true
+bytes.workspace = true
+futures-lite.workspace = true
+http-body-util.workspace = true
+http-body.workspace = true
http.workspace = true
itoa.workspace = true
pin-project-lite.workspace = true
@@ -33,7 +38,7 @@ serde_json = { workspace = true, optional = true }
[dev-dependencies]
anyhow.workspace = true
clap.workspace = true
-futures-lite.workspace = true
+http-body-util.workspace = true
futures-concurrency.workspace = true
humantime.workspace = true
serde = { workspace = true, features = ["derive"] }
@@ -65,6 +70,8 @@ authors = [
[workspace.dependencies]
anyhow = "1"
async-task = "4.7"
+async-trait = "*"
+bytes = "1.10.1"
cargo_metadata = "0.22"
clap = { version = "4.5.26", features = ["derive"] }
futures-core = "0.3.19"
@@ -73,6 +80,8 @@ futures-concurrency = "7.6"
humantime = "2.1.0"
heck = "0.5"
http = "1.1"
+http-body = "1.0.1"
+http-body-util = "0.1.3"
itoa = "1"
pin-project-lite = "0.2.8"
quote = "1.0"
diff --git a/examples/complex_http_client.rs b/examples/complex_http_client.rs
index 8a7b2b6..3754d60 100644
--- a/examples/complex_http_client.rs
+++ b/examples/complex_http_client.rs
@@ -1,9 +1,8 @@
use anyhow::{anyhow, Result};
use clap::{ArgAction, Parser};
use std::str::FromStr;
-use wstd::http::{
- body::BodyForthcoming, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri,
-};
+use wstd::http::{Body, BodyExt, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri};
+use wstd::io::AsyncWrite;
/// Complex HTTP client
///
@@ -86,23 +85,29 @@ async fn main() -> Result<()> {
trailers.insert(HeaderName::from_str(key)?, HeaderValue::from_str(value)?);
}
- // Send the request.
-
- let request = request.body(BodyForthcoming)?;
+ let body = if args.body {
+ Body::from_input_stream(wstd::io::stdin().into_inner()).into_boxed_body()
+ } else {
+ Body::empty().into_boxed_body()
+ };
+ let t = trailers.clone();
+ let body = body.with_trailers(async move {
+ if t.is_empty() {
+ None
+ } else {
+ Some(Ok(t))
+ }
+ });
+ let request = request.body(body)?;
+ // Send the request.
eprintln!("> {} / {:?}", request.method(), request.version());
for (key, value) in request.headers().iter() {
let value = String::from_utf8_lossy(value.as_bytes());
eprintln!("> {key}: {value}");
}
- let (mut outgoing_body, response) = client.start_request(request).await?;
-
- if args.body {
- wstd::io::copy(wstd::io::stdin(), &mut outgoing_body).await?;
- } else {
- wstd::io::copy(wstd::io::empty(), &mut outgoing_body).await?;
- }
+ let response = client.send(request).await?;
if !trailers.is_empty() {
eprintln!("...");
@@ -112,10 +117,6 @@ async fn main() -> Result<()> {
eprintln!("> {key}: {value}");
}
- Client::finish(outgoing_body, Some(trailers))?;
-
- let response = response.await?;
-
// Print the response.
eprintln!("< {:?} {}", response.version(), response.status());
@@ -124,10 +125,12 @@ async fn main() -> Result<()> {
eprintln!("< {key}: {value}");
}
- let mut body = response.into_body();
- wstd::io::copy(&mut body, wstd::io::stdout()).await?;
+ let body = response.into_body().into_http_body().collect().await?;
+ let trailers = body.trailers().cloned();
+ wstd::io::stdout()
+ .write_all(body.to_bytes().as_ref())
+ .await?;
- let trailers = body.finish().await?;
if let Some(trailers) = trailers {
for (key, value) in trailers.iter() {
let value = String::from_utf8_lossy(value.as_bytes());
diff --git a/examples/http_client.rs b/examples/http_client.rs
index 12bc685..2153f41 100644
--- a/examples/http_client.rs
+++ b/examples/http_client.rs
@@ -1,10 +1,7 @@
use anyhow::{anyhow, Result};
use clap::{ArgAction, Parser};
-use wstd::http::{
- body::{IncomingBody, StreamedBody},
- request::Builder,
- Body, Client, Method, Request, Response, Uri,
-};
+use wstd::http::{Body, BodyExt, Client, Method, Request, Uri};
+use wstd::io::AsyncWrite;
/// Simple HTTP client
///
@@ -75,39 +72,35 @@ async fn main() -> Result<()> {
// Send the request.
- async fn send_request(
- client: &Client,
- request: Builder,
- body: B,
- ) -> Result> {
- let request = request.body(body)?;
+ let body = if args.body {
+ Body::from_input_stream(wstd::io::stdin().into_inner())
+ } else {
+ Body::empty()
+ };
- eprintln!("> {} / {:?}", request.method(), request.version());
- for (key, value) in request.headers().iter() {
- let value = String::from_utf8_lossy(value.as_bytes());
- eprintln!("> {key}: {value}");
- }
+ let request = request.body(body)?;
- Ok(client.send(request).await?)
+ eprintln!("> {} / {:?}", request.method(), request.version());
+ for (key, value) in request.headers().iter() {
+ let value = String::from_utf8_lossy(value.as_bytes());
+ eprintln!("> {key}: {value}");
}
- let response = if args.body {
- send_request(&client, request, StreamedBody::new(wstd::io::stdin())).await
- } else {
- send_request(&client, request, wstd::io::empty()).await
- }?;
- // Print the response.
+ let response = client.send(request).await?;
+ // Print the response.
eprintln!("< {:?} {}", response.version(), response.status());
for (key, value) in response.headers().iter() {
let value = String::from_utf8_lossy(value.as_bytes());
eprintln!("< {key}: {value}");
}
- let mut body = response.into_body();
- wstd::io::copy(&mut body, wstd::io::stdout()).await?;
+ let body = response.into_body().into_http_body().collect().await?;
+ let trailers = body.trailers().cloned();
+ wstd::io::stdout()
+ .write_all(body.to_bytes().as_ref())
+ .await?;
- let trailers = body.finish().await?;
if let Some(trailers) = trailers {
for (key, value) in trailers.iter() {
let value = String::from_utf8_lossy(value.as_bytes());
diff --git a/examples/http_server.rs b/examples/http_server.rs
index b21eda4..d4df71c 100644
--- a/examples/http_server.rs
+++ b/examples/http_server.rs
@@ -1,31 +1,36 @@
-use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody};
-use wstd::http::server::{Finished, Responder};
-use wstd::http::{IntoBody, Request, Response, StatusCode};
-use wstd::io::{copy, empty, AsyncWrite};
+use anyhow::{Context, Result};
+use futures_lite::stream::once_future;
+use http_body_util::{BodyExt, StreamBody};
+use wstd::http::body::{Body, Bytes, Frame, Incoming};
+use wstd::http::{Error, HeaderMap, Request, Response, StatusCode};
use wstd::time::{Duration, Instant};
#[wstd::http_server]
-async fn main(request: Request, responder: Responder) -> Finished {
- match request.uri().path_and_query().unwrap().as_str() {
- "/wait" => http_wait(request, responder).await,
- "/echo" => http_echo(request, responder).await,
- "/echo-headers" => http_echo_headers(request, responder).await,
- "/echo-trailers" => http_echo_trailers(request, responder).await,
- "/fail" => http_fail(request, responder).await,
- "/bigfail" => http_bigfail(request, responder).await,
- "/" => http_home(request, responder).await,
- _ => http_not_found(request, responder).await,
+async fn main(request: Request) -> Result, Error> {
+ let path = request.uri().path_and_query().unwrap().as_str();
+ println!("serving {path}");
+ match path {
+ "/" => http_home(request).await,
+ "/wait-response" => http_wait_response(request).await,
+ "/wait-body" => http_wait_body(request).await,
+ "/echo" => http_echo(request).await,
+ "/echo-headers" => http_echo_headers(request).await,
+ "/echo-trailers" => http_echo_trailers(request).await,
+ "/response-status" => http_response_status(request).await,
+ "/response-fail" => http_response_fail(request).await,
+ "/response-body-fail" => http_body_fail(request).await,
+ _ => http_not_found(request).await,
}
}
-async fn http_home(_request: Request, responder: Responder) -> Finished {
+async fn http_home(_request: Request) -> Result> {
// To send a single string as the response body, use `Responder::respond`.
- responder
- .respond(Response::new("Hello, wasi:http/proxy world!\n".into_body()))
- .await
+ Ok(Response::new(
+ "Hello, wasi:http/proxy world!\n".to_owned().into(),
+ ))
}
-async fn http_wait(_request: Request, responder: Responder) -> Finished {
+async fn http_wait_response(_request: Request) -> Result> {
// Get the time now
let now = Instant::now();
@@ -35,60 +40,85 @@ async fn http_wait(_request: Request, responder: Responder) -> Fin
// Compute how long we slept for.
let elapsed = Instant::now().duration_since(now).as_millis();
- // To stream data to the response body, use `Responder::start_response`.
- let mut body = responder.start_response(Response::new(BodyForthcoming));
- let result = body
- .write_all(format!("slept for {elapsed} millis\n").as_bytes())
- .await;
- Finished::finish(body, result, None)
+ Ok(Response::new(
+ format!("slept for {elapsed} millis\n").into(),
+ ))
}
-async fn http_echo(mut request: Request, responder: Responder) -> Finished {
- // Stream data from the request body to the response body.
- let mut body = responder.start_response(Response::new(BodyForthcoming));
- let result = copy(request.body_mut(), &mut body).await;
- Finished::finish(body, result, None)
-}
+async fn http_wait_body(_request: Request) -> Result> {
+ // Get the time now
+ let now = Instant::now();
-async fn http_fail(_request: Request, responder: Responder) -> Finished {
- let body = responder.start_response(Response::new(BodyForthcoming));
- Finished::fail(body)
-}
+ let body = StreamBody::new(once_future(async move {
+ // Sleep for one second.
+ wstd::task::sleep(Duration::from_secs(1)).await;
-async fn http_bigfail(_request: Request, responder: Responder) -> Finished {
- async fn write_body(body: &mut OutgoingBody) -> wstd::io::Result<()> {
- for _ in 0..0x10 {
- body.write_all("big big big big\n".as_bytes()).await?;
- }
- body.flush().await?;
- Ok(())
- }
+ // Compute how long we slept for.
+ let elapsed = Instant::now().duration_since(now).as_millis();
+ anyhow::Ok(Frame::data(Bytes::from(format!(
+ "slept for {elapsed} millis\n"
+ ))))
+ }));
+
+ Ok(Response::new(body.into()))
+}
- let mut body = responder.start_response(Response::new(BodyForthcoming));
- let _ = write_body(&mut body).await;
- Finished::fail(body)
+async fn http_echo(request: Request) -> Result> {
+ let (_parts, body) = request.into_parts();
+ Ok(Response::new(body.into()))
}
-async fn http_echo_headers(request: Request, responder: Responder) -> Finished {
+async fn http_echo_headers(request: Request) -> Result> {
let mut response = Response::builder();
*response.headers_mut().unwrap() = request.into_parts().0.headers;
- let response = response.body(empty()).unwrap();
- responder.respond(response).await
+ Ok(response.body("".to_owned().into())?)
}
-async fn http_echo_trailers(request: Request, responder: Responder) -> Finished {
- let body = responder.start_response(Response::new(BodyForthcoming));
- let (trailers, result) = match request.into_body().finish().await {
- Ok(trailers) => (trailers, Ok(())),
- Err(err) => (Default::default(), Err(std::io::Error::other(err))),
+async fn http_echo_trailers(request: Request) -> Result> {
+ let collected = request.into_body().into_http_body().collect().await?;
+ let trailers = collected.trailers().cloned().unwrap_or_else(|| {
+ let mut trailers = HeaderMap::new();
+ trailers.insert("x-no-trailers", "1".parse().unwrap());
+ trailers
+ });
+
+ let body = StreamBody::new(once_future(async move {
+ anyhow::Ok(Frame::::trailers(trailers))
+ }));
+ Ok(Response::new(body.into()))
+}
+
+async fn http_response_status(request: Request) -> Result> {
+ let status = if let Some(header_val) = request.headers().get("x-response-status") {
+ header_val
+ .to_str()
+ .context("contents of x-response-status")?
+ .parse::()
+ .context("u16 value from x-response-status")?
+ } else {
+ 500
};
- Finished::finish(body, result, trailers)
+ Ok(Response::builder()
+ .status(status)
+ .body(String::new().into())?)
+}
+
+async fn http_response_fail(_request: Request) -> Result> {
+ Err(anyhow::anyhow!("error creating response"))
+}
+
+async fn http_body_fail(_request: Request) -> Result> {
+ let body = StreamBody::new(once_future(async move {
+ Err::, _>(anyhow::anyhow!("error creating body"))
+ }));
+
+ Ok(Response::new(body.into()))
}
-async fn http_not_found(_request: Request, responder: Responder) -> Finished {
+async fn http_not_found(_request: Request) -> Result> {
let response = Response::builder()
.status(StatusCode::NOT_FOUND)
- .body(empty())
+ .body(Body::empty())
.unwrap();
- responder.respond(response).await
+ Ok(response)
}
diff --git a/macro/src/lib.rs b/macro/src/lib.rs
index cdc477c..011bfa9 100644
--- a/macro/src/lib.rs
+++ b/macro/src/lib.rs
@@ -92,10 +92,8 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// ```ignore
/// #[wstd::http_server]
-/// async fn main(request: Request, responder: Responder) -> Finished {
-/// responder
-/// .respond(Response::new("Hello!\n".into_body()))
-/// .await
+/// async fn main(request: Request) -> Result> {
+/// Ok(Response::new("Hello!\n".into_body()))
/// }
/// ```
#[proc_macro_attribute]
@@ -137,12 +135,15 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr
}
let responder = ::wstd::http::server::Responder::new(response_out);
- let _finished: ::wstd::http::server::Finished =
- match ::wstd::http::request::try_from_incoming(request)
- {
- Ok(request) => ::wstd::runtime::block_on(async { __run(request, responder).await }),
- Err(err) => responder.fail(err),
- };
+ ::wstd::runtime::block_on(async move {
+ match ::wstd::http::request::try_from_incoming(request) {
+ Ok(request) => match __run(request).await {
+ Ok(response) => { responder.respond(response).await.unwrap() },
+ Err(err) => responder.fail(err).unwrap(),
+ }
+ Err(err) => responder.fail(err).unwrap(),
+ }
+ })
}
}
diff --git a/src/http/body.rs b/src/http/body.rs
index 9eecfae..ec364ef 100644
--- a/src/http/body.rs
+++ b/src/http/body.rs
@@ -1,353 +1,500 @@
-//! HTTP body types
+use crate::http::{
+ error::Context as _,
+ fields::{header_map_from_wasi, header_map_to_wasi},
+ Error, HeaderMap,
+};
+use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncWrite};
+use crate::runtime::{AsyncPollable, Reactor, WaitFor};
+
+pub use ::http_body::{Body as HttpBody, Frame, SizeHint};
+pub use bytes::Bytes;
-use crate::http::fields::header_map_from_wasi;
-use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty};
-use crate::runtime::AsyncPollable;
-use core::fmt;
use http::header::CONTENT_LENGTH;
-use wasip2::http::types::IncomingBody as WasiIncomingBody;
+use http_body_util::{combinators::UnsyncBoxBody, BodyExt};
+use std::fmt;
+use std::future::{poll_fn, Future};
+use std::pin::{pin, Pin};
+use std::task::{Context, Poll};
+use wasip2::http::types::{
+ FutureTrailers, IncomingBody as WasiIncomingBody, OutgoingBody as WasiOutgoingBody,
+};
+use wasip2::io::streams::{InputStream as WasiInputStream, StreamError};
-#[cfg(feature = "json")]
-use serde::de::DeserializeOwned;
-#[cfg(feature = "json")]
-use serde_json;
+pub mod util {
+ pub use http_body_util::*;
+}
-pub use super::{
- error::{Error, ErrorVariant},
- HeaderMap,
-};
+#[derive(Debug)]
+pub struct Body(pub(crate) BodyInner);
#[derive(Debug)]
-pub(crate) enum BodyKind {
- Fixed(u64),
- Chunked,
+pub(crate) enum BodyInner {
+ Boxed(UnsyncBoxBody),
+ Incoming(Incoming),
+ Complete(Bytes),
}
-impl BodyKind {
- pub(crate) fn from_headers(headers: &HeaderMap) -> Result {
- if let Some(value) = headers.get(CONTENT_LENGTH) {
- let content_length = std::str::from_utf8(value.as_ref())
- .unwrap()
- .parse::()
- .map_err(|_| InvalidContentLength)?;
- Ok(BodyKind::Fixed(content_length))
- } else {
- Ok(BodyKind::Chunked)
+impl Body {
+ pub async fn send(self, outgoing_body: WasiOutgoingBody) -> Result<(), Error> {
+ match self.0 {
+ BodyInner::Incoming(incoming) => {
+ let in_body = incoming.into_inner();
+ let mut in_stream =
+ AsyncInputStream::new(in_body.stream().expect("incoming body already read"));
+ let mut out_stream = AsyncOutputStream::new(
+ outgoing_body
+ .write()
+ .expect("outgoing body already written"),
+ );
+ crate::io::copy(&mut in_stream, &mut out_stream)
+ .await
+ .map_err(|e| {
+ Error::from(e)
+ .context("copying incoming body stream to outgoing body stream")
+ })?;
+ drop(in_stream);
+ drop(out_stream);
+ let future_in_trailers = WasiIncomingBody::finish(in_body);
+ Reactor::current()
+ .schedule(future_in_trailers.subscribe())
+ .wait_for()
+ .await;
+ let in_trailers: Option = future_in_trailers
+ .get()
+ .expect("pollable ready")
+ .expect("got once")
+ .map_err(|e| Error::from(e).context("recieving incoming trailers"))?;
+ WasiOutgoingBody::finish(outgoing_body, in_trailers)
+ .map_err(|e| Error::from(e).context("finishing outgoing body"))?;
+ Ok(())
+ }
+ BodyInner::Boxed(box_body) => {
+ let mut out_stream = AsyncOutputStream::new(
+ outgoing_body
+ .write()
+ .expect("outgoing body already written"),
+ );
+ let mut body = pin!(box_body);
+ let mut trailers = None;
+ loop {
+ match poll_fn(|cx| body.as_mut().poll_frame(cx)).await {
+ Some(Ok(frame)) if frame.is_data() => {
+ let data = frame.data_ref().unwrap();
+ out_stream.write_all(data).await?;
+ }
+ Some(Ok(frame)) if frame.is_trailers() => {
+ trailers =
+ Some(header_map_to_wasi(frame.trailers_ref().unwrap()).map_err(
+ |e| Error::from(e).context("outoging trailers to wasi"),
+ )?);
+ }
+ Some(Err(err)) => break Err(err.context("sending outgoing body")),
+ None => {
+ drop(out_stream);
+ WasiOutgoingBody::finish(outgoing_body, trailers)
+ .map_err(|e| Error::from(e).context("finishing outgoing body"))?;
+ break Ok(());
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+ BodyInner::Complete(bytes) => {
+ let mut out_stream = AsyncOutputStream::new(
+ outgoing_body
+ .write()
+ .expect("outgoing body already written"),
+ );
+ out_stream.write_all(&bytes).await?;
+ drop(out_stream);
+ WasiOutgoingBody::finish(outgoing_body, None)
+ .map_err(|e| Error::from(e).context("finishing outgoing body"))?;
+ Ok(())
+ }
}
}
-}
-/// A trait representing an HTTP body.
-#[doc(hidden)]
-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.
- fn is_empty(&self) -> bool {
- matches!(self.len(), Some(0))
+ pub fn into_boxed_body(self) -> UnsyncBoxBody {
+ match self.0 {
+ BodyInner::Incoming(i) => i.into_http_body().boxed_unsync(),
+ BodyInner::Complete(bytes) => http_body_util::Full::new(bytes)
+ .map_err(annotate_err)
+ .boxed_unsync(),
+ BodyInner::Boxed(b) => b,
+ }
}
-}
-/// Conversion into a `Body`.
-#[doc(hidden)]
-pub trait IntoBody {
- /// What type of `Body` are we turning this into?
- type IntoBody: Body;
- /// Convert into `Body`.
- fn into_body(self) -> Self::IntoBody;
-}
-impl IntoBody for T
-where
- T: Body,
-{
- type IntoBody = T;
- fn into_body(self) -> Self::IntoBody {
- self
+ pub fn as_boxed_body(&mut self) -> &mut UnsyncBoxBody {
+ let mut prev = Self::empty();
+ std::mem::swap(self, &mut prev);
+ self.0 = BodyInner::Boxed(prev.into_boxed_body());
+
+ match &mut self.0 {
+ BodyInner::Boxed(ref mut b) => b,
+ _ => unreachable!(),
+ }
}
-}
-impl IntoBody for String {
- type IntoBody = BoundedBody>;
- fn into_body(self) -> Self::IntoBody {
- BoundedBody(Cursor::new(self.into_bytes()))
+ pub async fn contents(&mut self) -> Result<&[u8], Error> {
+ match &mut self.0 {
+ BodyInner::Complete(ref bs) => Ok(bs.as_ref()),
+ inner => {
+ let mut prev = BodyInner::Complete(Bytes::new());
+ std::mem::swap(inner, &mut prev);
+ let boxed_body = match prev {
+ BodyInner::Incoming(i) => i.into_http_body().boxed_unsync(),
+ BodyInner::Boxed(b) => b,
+ BodyInner::Complete(_) => unreachable!(),
+ };
+ let collected = boxed_body.collect().await?;
+ *inner = BodyInner::Complete(collected.to_bytes());
+ Ok(match inner {
+ BodyInner::Complete(ref bs) => bs.as_ref(),
+ _ => unreachable!(),
+ })
+ }
+ }
}
-}
-impl IntoBody for &str {
- type IntoBody = BoundedBody>;
- fn into_body(self) -> Self::IntoBody {
- BoundedBody(Cursor::new(self.to_owned().into_bytes()))
+ pub fn content_length(&self) -> Option {
+ match &self.0 {
+ BodyInner::Boxed(b) => b.size_hint().exact(),
+ BodyInner::Complete(bs) => Some(bs.len() as u64),
+ BodyInner::Incoming(i) => i.size_hint.content_length(),
+ }
}
-}
-impl IntoBody for Vec {
- type IntoBody = BoundedBody>;
- fn into_body(self) -> Self::IntoBody {
- BoundedBody(Cursor::new(self))
+ pub fn empty() -> Self {
+ Body(BodyInner::Complete(Bytes::new()))
}
-}
-impl IntoBody for &[u8] {
- type IntoBody = BoundedBody>;
- fn into_body(self) -> Self::IntoBody {
- BoundedBody(Cursor::new(self.to_owned()))
+ pub fn from_string(s: impl Into) -> Self {
+ let s = s.into();
+ Body(BodyInner::Complete(Bytes::from_owner(s.into_bytes())))
}
-}
-/// An HTTP body with a known length
-#[derive(Debug)]
-pub struct BoundedBody(Cursor);
+ pub async fn str_contents(&mut self) -> Result<&str, Error> {
+ let bs = self.contents().await?;
+ std::str::from_utf8(bs).context("decoding body contents as string")
+ }
-impl> AsyncRead for BoundedBody {
- async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result {
- self.0.read(buf).await
+ pub fn from_bytes(b: impl Into) -> Self {
+ let b = b.into();
+ Body::from(http_body_util::Full::new(b))
}
-}
-impl> Body for BoundedBody {
- fn len(&self) -> Option {
- Some(self.0.get_ref().as_ref().len())
+
+ #[cfg(feature = "json")]
+ pub fn from_json(data: &T) -> Result {
+ Ok(Self::from_string(serde_json::to_string(data)?))
}
-}
-/// An HTTP body with an unknown length
-#[derive(Debug)]
-pub struct StreamedBody(S);
+ #[cfg(feature = "json")]
+ pub async fn json serde::Deserialize<'a>>(&mut self) -> Result {
+ let str = self.str_contents().await?;
+ serde_json::from_str(str).context("decoding body contents as json")
+ }
-impl StreamedBody {
- /// Wrap an `AsyncRead` impl in a type that provides a [`Body`] implementation.
- pub fn new(s: S) -> Self {
- Self(s)
+ pub fn from_input_stream(r: crate::io::AsyncInputStream) -> Self {
+ use futures_lite::stream::StreamExt;
+ Body(BodyInner::Boxed(http_body_util::BodyExt::boxed_unsync(
+ http_body_util::StreamBody::new(r.into_stream().map(|res| {
+ res.map(|bytevec| Frame::data(Bytes::from_owner(bytevec)))
+ .map_err(Into::into)
+ })),
+ )))
}
}
-impl AsyncRead for StreamedBody {
- async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result {
- self.0.read(buf).await
- }
+
+fn annotate_err(_: E) -> Error {
+ unreachable!()
}
-impl Body for StreamedBody {
- fn len(&self) -> Option {
- None
+
+impl From for Body
+where
+ B: HttpBody + Send + 'static,
+ ::Data: Into,
+ ::Error: Into,
+{
+ fn from(http_body: B) -> Body {
+ use util::BodyExt;
+ Body(BodyInner::Boxed(
+ http_body
+ .map_frame(|f| f.map_data(Into::into))
+ .map_err(Into::into)
+ .boxed_unsync(),
+ ))
}
}
-impl Body for Empty {
- fn len(&self) -> Option {
- Some(0)
+impl From for Body {
+ fn from(incoming: Incoming) -> Body {
+ Body(BodyInner::Incoming(incoming))
}
}
-/// An incoming HTTP body
#[derive(Debug)]
-pub struct IncomingBody {
- kind: BodyKind,
- // IMPORTANT: the order of these fields here matters. `body_stream` must
- // be dropped before `incoming_body`.
- body_stream: AsyncInputStream,
- incoming_body: WasiIncomingBody,
+pub struct Incoming {
+ body: WasiIncomingBody,
+ size_hint: BodyHint,
}
-impl IncomingBody {
- pub(crate) fn new(
- kind: BodyKind,
- body_stream: AsyncInputStream,
- incoming_body: WasiIncomingBody,
- ) -> Self {
- Self {
- kind,
- body_stream,
- incoming_body,
- }
+impl Incoming {
+ pub(crate) fn new(body: WasiIncomingBody, size_hint: BodyHint) -> Self {
+ Self { body, size_hint }
}
-
- /// Consume this `IncomingBody` and return the trailers, if present.
- pub async fn finish(self) -> Result