Skip to content
Merged
Changes from 8 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
173 changes: 172 additions & 1 deletion crates/wasi-http/src/p3/request.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use crate::get_content_length;
use crate::p3::bindings::http::types::ErrorCode;
use crate::p3::body::Body;
use crate::p3::body::{Body, GuestBody};
use crate::p3::{WasiHttpView, WasiHttpCtxView};
use bytes::Bytes;
use wasmtime::AsContextMut;
use core::time::Duration;
use http::uri::{Authority, PathAndQuery, Scheme};
use http::{HeaderMap, Method};
use http_body_util::BodyExt as _;
use http_body_util::combinators::BoxBody;
use std::sync::Arc;
use tokio::sync::oneshot;
use anyhow::Context;

/// The concrete type behind a `wasi:http/types/request-options` resource.
#[derive(Copy, Clone, Debug, Default)]
Expand Down Expand Up @@ -38,6 +42,44 @@ pub struct Request {
pub(crate) body: Body,
}

impl TryFrom<Request> for http::Request<Body> {
type Error = http::Error;

fn try_from(
Request {
method,
scheme,
authority,
path_with_query,
headers,
options: _,
body,
}: Request,
) -> Result<Self, Self::Error> {
// Reconstruct URI from its components
let mut uri_builder = http::Uri::builder();
if let Some(s) = scheme {
uri_builder = uri_builder.scheme(s);
}
if let Some(a) = authority {
uri_builder = uri_builder.authority(a.as_str());
}
if let Some(pq) = path_with_query {
uri_builder = uri_builder.path_and_query(pq.as_str());
}
let uri: http::Uri = uri_builder.build()?;

let mut req = http::Request::builder().method(method).uri(uri);

if let Some(headers_mut) = req.headers_mut() {
*headers_mut = Arc::unwrap_or_clone(headers);
} else {
tracing::warn!("failed to get mutable headers from http request builder");
}
req.body(body)
}
}

impl Request {
/// Construct a new [Request]
///
Expand Down Expand Up @@ -119,6 +161,59 @@ impl Request {
body.map_err(Into::into).boxed(),
)
}

/// Convert this [`Request`] into an [`http::Request<BoxBody<Bytes, ErrorCode>>`].
///
/// The specified future `fut` can be used to communicate a request processing
/// error, if any, back to the caller (e.g., if this request was constructed
/// through `wasi:http/types.request#new`).
pub fn into_http<T: WasiHttpView + 'static>(
self,
store: impl AsContextMut<Data = T>,
fut: impl Future<Output = Result<(), ErrorCode>> + Send + 'static,
) -> wasmtime::Result<http::Request<BoxBody<Bytes, ErrorCode>>> {
self.into_http_with_getter(store, fut, T::http)
}

/// Like [`Self::into_http`], but uses a custom getter for obtaining the [`WasiHttpCtxView`].
pub fn into_http_with_getter<T: 'static>(
self,
store: impl AsContextMut<Data = T>,
fut: impl Future<Output = Result<(), ErrorCode>> + Send + 'static,
getter: fn(&mut T) -> WasiHttpCtxView<'_>,
) -> wasmtime::Result<http::Request<BoxBody<Bytes, ErrorCode>>> {
let req = http::Request::try_from(self)?;
let (req, body) = req.into_parts();

let body = match body {
Body::Guest {
contents_rx,
trailers_rx,
result_tx,
} => {
// Validate Content-Length if present
let content_length = get_content_length(&req.headers)
.context("failed to parse `content-length`")?;
GuestBody::new(
store,
contents_rx,
trailers_rx,
result_tx,
fut,
content_length,
ErrorCode::HttpRequestBodySize,
getter,
)
.boxed()
}
Body::Host { body, result_tx } => {
_ = result_tx.send(Box::new(fut));
body
}
};

Ok(http::Request::from_parts(req, body))
}
}

/// The default implementation of how an outgoing request is sent.
Expand Down Expand Up @@ -348,3 +443,79 @@ pub async fn default_send_request(
conn.await.map_err(ErrorCode::from_hyper_response_error)
}))
}

#[cfg(test)]
mod tests {
use super::*;
use http_body_util::{BodyExt, Full};
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
use crate::p3::WasiHttpCtx;
use wasmtime::{Engine, Store};

struct TestHttpCtx;
struct TestCtx {
table: ResourceTable,
wasi: WasiCtx,
http: TestHttpCtx,
}

impl TestCtx {
fn new() -> Self {
Self {
table: ResourceTable::default(),
wasi: WasiCtxBuilder::new().build(),
http: TestHttpCtx,
}
}
}

impl WasiView for TestCtx {
fn ctx(&mut self) -> WasiCtxView<'_> {
WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.table,
}
}
}

impl WasiHttpCtx for TestHttpCtx {}

impl WasiHttpView for TestCtx {
fn http(&mut self) -> WasiHttpCtxView<'_> {
WasiHttpCtxView {
ctx: &mut self.http,
table: &mut self.table,
}
}
}

#[tokio::test]
async fn test_request_into_http() -> Result<(), anyhow::Error> {
let (req, fut) = Request::new(
Method::GET,
Some(Scheme::HTTPS),
Some(Authority::from_static("example.com")),
Some(PathAndQuery::from_static("/path?query=1")),
HeaderMap::new(),
None,
Full::new(Bytes::from_static(b"body")).map_err(|_| unreachable!()).boxed(),
);

let engine = Engine::default();
let mut store = Store::new(
&engine,
TestCtx::new(),
);
let http_req = req.into_http(&mut store, fut).unwrap();
assert_eq!(http_req.method(), Method::GET);
assert_eq!(
http_req.uri(),
&http::Uri::from_static("https://example.com/path?query=1")
);
let body_bytes = http_req.into_body().collect()
.await?;

assert_eq!(*body_bytes.to_bytes(), *b"body");
Ok(())
}
}
Loading