From d7be6a9d034733f879c85f81c5109fe6f8170061 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 12:09:08 -0700 Subject: [PATCH 1/7] Sequester WASIp2 in `wasmtime-wasi-http` to a module This mirrors the `wasmtime-wasi` crate's organization where there's a `p2` module and a `p3` module at the top level. --- .github/workflows/main.yml | 1 + crates/wasi-http/Cargo.toml | 3 +- crates/wasi-http/src/ctx.rs | 35 ++ crates/wasi-http/src/handler.rs | 2 +- crates/wasi-http/src/lib.rs | 402 ++------------------ crates/wasi-http/src/{ => p2}/bindings.rs | 8 +- crates/wasi-http/src/{ => p2}/body.rs | 4 +- crates/wasi-http/src/{ => p2}/error.rs | 4 +- crates/wasi-http/src/{ => p2}/http_impl.rs | 6 +- crates/wasi-http/src/p2/mod.rs | 376 ++++++++++++++++++ crates/wasi-http/src/{ => p2}/types.rs | 68 +--- crates/wasi-http/src/{ => p2}/types_impl.rs | 37 +- crates/wasi-http/src/p3/mod.rs | 2 +- crates/wasi-http/tests/all/p2.rs | 17 +- crates/wasi-http/tests/all/p2/async_.rs | 2 +- crates/wasi-http/tests/all/p2/sync.rs | 2 +- crates/wasi-http/tests/all/p3/mod.rs | 2 +- crates/wasi/Cargo.toml | 3 + src/commands/run.rs | 12 +- src/commands/serve.rs | 15 +- 20 files changed, 507 insertions(+), 494 deletions(-) create mode 100644 crates/wasi-http/src/ctx.rs rename crates/wasi-http/src/{ => p2}/bindings.rs (93%) rename crates/wasi-http/src/{ => p2}/body.rs (99%) rename crates/wasi-http/src/{ => p2}/error.rs (96%) rename crates/wasi-http/src/{ => p2}/http_impl.rs (96%) create mode 100644 crates/wasi-http/src/p2/mod.rs rename crates/wasi-http/src/{ => p2}/types.rs (92%) rename crates/wasi-http/src/{ => p2}/types_impl.rs (95%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 782cbe97c3cc..508d9efb2390 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -413,6 +413,7 @@ jobs: - name: wasmtime-wasi-http checks: | -p wasmtime-wasi-http --no-default-features + -p wasmtime-wasi-http --no-default-features --features p2 -p wasmtime-wasi-http --no-default-features --features p3 -p wasmtime-wasi-http --no-default-features --features p3 --all-targets diff --git a/crates/wasi-http/Cargo.toml b/crates/wasi-http/Cargo.toml index 042e04a2f3a9..7eb31ae2de4e 100644 --- a/crates/wasi-http/Cargo.toml +++ b/crates/wasi-http/Cargo.toml @@ -15,8 +15,9 @@ workspace = true all-features = true [features] -default = ["default-send-request"] +default = ["default-send-request", "p2"] default-send-request = ["dep:tokio-rustls", "dep:rustls", "dep:webpki-roots"] +p2 = ["wasmtime-wasi/p2"] p3 = ["wasmtime-wasi/p3", "dep:tokio-util"] component-model-async = ["futures/alloc", "wasmtime/component-model-async"] diff --git a/crates/wasi-http/src/ctx.rs b/crates/wasi-http/src/ctx.rs new file mode 100644 index 000000000000..bf4ff50672ef --- /dev/null +++ b/crates/wasi-http/src/ctx.rs @@ -0,0 +1,35 @@ +/// Default maximum size for the contents of a fields resource. +/// +/// Typically, HTTP proxies limit headers to 8k. This number is higher than that +/// because it not only includes the wire-size of headers but it additionally +/// includes factors for the in-memory representation of `HeaderMap`. This is in +/// theory high enough that no one runs into it but low enough such that a +/// completely full `HeaderMap` doesn't break the bank in terms of memory +/// consumption. +const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024; + +/// Capture the state necessary for use in the wasi-http API implementation. +#[derive(Debug)] +pub struct WasiHttpCtx { + pub(crate) field_size_limit: usize, +} + +impl WasiHttpCtx { + /// Create a new context. + pub fn new() -> Self { + Self { + field_size_limit: DEFAULT_FIELD_SIZE_LIMIT, + } + } + + /// Set the maximum size for any fields resources created by this context. + /// + /// The limit specified here is roughly a byte limit for the size of the + /// in-memory representation of headers. This means that the limit needs to + /// be larger than the literal representation of headers on the wire to + /// account for in-memory Rust-side data structures representing the header + /// names/values/etc. + pub fn set_field_size_limit(&mut self, limit: usize) { + self.field_size_limit = limit; + } +} diff --git a/crates/wasi-http/src/handler.rs b/crates/wasi-http/src/handler.rs index c8496710d199..9b08c0b19bdd 100644 --- a/crates/wasi-http/src/handler.rs +++ b/crates/wasi-http/src/handler.rs @@ -35,7 +35,7 @@ pub mod p2 { require_store_data_send: true, with: { // http is in this crate - "wasi:http": crate::bindings::http, + "wasi:http": crate::p2::bindings::http, // Upstream package dependencies "wasi:io": wasmtime_wasi::p2::bindings::io, } diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs index 358682ddc91b..cdf8648ac37a 100644 --- a/crates/wasi-http/src/lib.rs +++ b/crates/wasi-http/src/lib.rs @@ -1,402 +1,50 @@ -//! # Wasmtime's WASI HTTP Implementation +//! Wasmtime's implementation of `wasi:http` //! -//! This crate is Wasmtime's host implementation of the `wasi:http` package as -//! part of WASIp2. This crate's implementation is primarily built on top of -//! [`hyper`] and [`tokio`]. -//! -//! # WASI HTTP Interfaces -//! -//! This crate contains implementations of the following interfaces: -//! -//! * [`wasi:http/incoming-handler`] -//! * [`wasi:http/outgoing-handler`] -//! * [`wasi:http/types`] -//! -//! The crate also contains an implementation of the [`wasi:http/proxy`] world. -//! -//! [`wasi:http/proxy`]: crate::bindings::Proxy -//! [`wasi:http/outgoing-handler`]: crate::bindings::http::outgoing_handler::Host -//! [`wasi:http/types`]: crate::bindings::http::types::Host -//! [`wasi:http/incoming-handler`]: crate::bindings::exports::wasi::http::incoming_handler::Guest -//! -//! This crate is very similar to [`wasmtime_wasi`] in the it uses the -//! `bindgen!` macro in Wasmtime to generate bindings to interfaces. Bindings -//! are located in the [`bindings`] module. -//! -//! # The `WasiHttpView` trait -//! -//! All `bindgen!`-generated `Host` traits are implemented in terms of a -//! [`WasiHttpView`] trait which provides basic access to [`WasiHttpCtx`], -//! configuration for WASI HTTP, and a [`wasmtime_wasi::ResourceTable`], the -//! state for all host-defined component model resources. -//! -//! The [`WasiHttpView`] trait additionally offers a few other configuration -//! methods such as [`WasiHttpView::send_request`] to customize how outgoing -//! HTTP requests are handled. -//! -//! # Async and Sync -//! -//! There are both asynchronous and synchronous bindings in this crate. For -//! example [`add_to_linker_async`] is for asynchronous embedders and -//! [`add_to_linker_sync`] is for synchronous embedders. Note that under the -//! hood both versions are implemented with `async` on top of [`tokio`]. -//! -//! # Examples -//! -//! Usage of this crate is done through a few steps to get everything hooked up: -//! -//! 1. First implement [`WasiHttpView`] for your type which is the `T` in -//! [`wasmtime::Store`]. -//! 2. Add WASI HTTP interfaces to a [`wasmtime::component::Linker`]. There -//! are a few options of how to do this: -//! * Use [`add_to_linker_async`] to bundle all interfaces in -//! `wasi:http/proxy` together -//! * Use [`add_only_http_to_linker_async`] to add only HTTP interfaces but -//! no others. This is useful when working with -//! [`wasmtime_wasi::p2::add_to_linker_async`] for example. -//! * Add individual interfaces such as with the -//! [`bindings::http::outgoing_handler::add_to_linker`] function. -//! 3. Use [`ProxyPre`](bindings::ProxyPre) to pre-instantiate a component -//! before serving requests. -//! 4. When serving requests use -//! [`ProxyPre::instantiate_async`](bindings::ProxyPre::instantiate_async) -//! to create instances and handle HTTP requests. -//! -//! A standalone example of doing all this looks like: -//! -//! ```no_run -//! use wasmtime::bail; -//! use hyper::server::conn::http1; -//! use std::sync::Arc; -//! use tokio::net::TcpListener; -//! use wasmtime::component::{Component, Linker, ResourceTable}; -//! use wasmtime::{Engine, Result, Store}; -//! use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -//! use wasmtime_wasi_http::bindings::ProxyPre; -//! use wasmtime_wasi_http::bindings::http::types::Scheme; -//! use wasmtime_wasi_http::body::HyperOutgoingBody; -//! use wasmtime_wasi_http::io::TokioIo; -//! use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -//! -//! #[tokio::main] -//! async fn main() -> Result<()> { -//! let component = std::env::args().nth(1).unwrap(); -//! -//! // Prepare the `Engine` for Wasmtime -//! let engine = Engine::default(); -//! -//! // Compile the component on the command line to machine code -//! let component = Component::from_file(&engine, &component)?; -//! -//! // Prepare the `ProxyPre` which is a pre-instantiated version of the -//! // component that we have. This will make per-request instantiation -//! // much quicker. -//! let mut linker = Linker::new(&engine); -//! wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; -//! wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; -//! let pre = ProxyPre::new(linker.instantiate_pre(&component)?)?; -//! -//! // Prepare our server state and start listening for connections. -//! let server = Arc::new(MyServer { pre }); -//! let listener = TcpListener::bind("127.0.0.1:8000").await?; -//! println!("Listening on {}", listener.local_addr()?); -//! -//! loop { -//! // Accept a TCP connection and serve all of its requests in a separate -//! // tokio task. Note that for now this only works with HTTP/1.1. -//! let (client, addr) = listener.accept().await?; -//! println!("serving new client from {addr}"); -//! -//! let server = server.clone(); -//! tokio::task::spawn(async move { -//! if let Err(e) = http1::Builder::new() -//! .keep_alive(true) -//! .serve_connection( -//! TokioIo::new(client), -//! hyper::service::service_fn(move |req| { -//! let server = server.clone(); -//! async move { server.handle_request(req).await } -//! }), -//! ) -//! .await -//! { -//! eprintln!("error serving client[{addr}]: {e:?}"); -//! } -//! }); -//! } -//! } -//! -//! struct MyServer { -//! pre: ProxyPre, -//! } -//! -//! impl MyServer { -//! async fn handle_request( -//! &self, -//! req: hyper::Request, -//! ) -> Result> { -//! // Create per-http-request state within a `Store` and prepare the -//! // initial resources passed to the `handle` function. -//! let mut store = Store::new( -//! self.pre.engine(), -//! MyClientState { -//! table: ResourceTable::new(), -//! wasi: WasiCtx::builder().inherit_stdio().build(), -//! http: WasiHttpCtx::new(), -//! }, -//! ); -//! let (sender, receiver) = tokio::sync::oneshot::channel(); -//! let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; -//! let out = store.data_mut().new_response_outparam(sender)?; -//! let pre = self.pre.clone(); -//! -//! // Run the http request itself in a separate task so the task can -//! // optionally continue to execute beyond after the initial -//! // headers/response code are sent. -//! let task = tokio::task::spawn(async move { -//! let proxy = pre.instantiate_async(&mut store).await?; -//! -//! if let Err(e) = proxy -//! .wasi_http_incoming_handler() -//! .call_handle(store, req, out) -//! .await -//! { -//! return Err(e); -//! } -//! -//! Ok(()) -//! }); -//! -//! match receiver.await { -//! // If the client calls `response-outparam::set` then one of these -//! // methods will be called. -//! Ok(Ok(resp)) => Ok(resp), -//! Ok(Err(e)) => Err(e.into()), -//! -//! // Otherwise the `sender` will get dropped along with the `Store` -//! // meaning that the oneshot will get disconnected and here we can -//! // inspect the `task` result to see what happened -//! Err(_) => { -//! let e = match task.await { -//! Ok(Ok(())) => { -//! bail!("guest never invoked `response-outparam::set` method") -//! } -//! Ok(Err(e)) => e, -//! Err(e) => e.into(), -//! }; -//! return Err(e.context("guest never invoked `response-outparam::set` method")); -//! } -//! } -//! } -//! } -//! -//! struct MyClientState { -//! wasi: WasiCtx, -//! http: WasiHttpCtx, -//! table: ResourceTable, -//! } -//! -//! impl WasiView for MyClientState { -//! fn ctx(&mut self) -> WasiCtxView<'_> { -//! WasiCtxView { ctx: &mut self.wasi, table: &mut self.table } -//! } -//! } -//! -//! impl WasiHttpView for MyClientState { -//! fn ctx(&mut self) -> &mut WasiHttpCtx { -//! &mut self.http -//! } -//! -//! fn table(&mut self) -> &mut ResourceTable { -//! &mut self.table -//! } -//! } -//! ``` +//! This crate is organized similarly to [`wasmtime_wasi`] where there is a +//! top-level [`p2`] and [`p3`] module corresponding to the implementation for +//! WASIp2 and WASIp3. #![deny(missing_docs)] #![doc(test(attr(deny(warnings))))] #![doc(test(attr(allow(dead_code, unused_variables, unused_mut))))] #![cfg_attr(docsrs, feature(doc_cfg))] -mod error; -mod http_impl; -mod types_impl; +use http::{HeaderName, header}; -pub mod body; +mod ctx; #[cfg(feature = "component-model-async")] pub mod handler; pub mod io; -pub mod types; - -pub mod bindings; - +#[cfg(feature = "p2")] +pub mod p2; #[cfg(feature = "p3")] pub mod p3; -pub use crate::error::{ - HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, -}; -#[doc(inline)] -pub use crate::types::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, - WasiHttpImpl, WasiHttpView, -}; -use http::header::CONTENT_LENGTH; -use wasmtime::component::{HasData, Linker}; - -/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. -/// -/// This function will add the `async` variant of all interfaces into the -/// `Linker` provided. For embeddings with async support disabled see -/// [`add_to_linker_sync`] instead. -/// -/// # Example -/// -/// ``` -/// use wasmtime::{Engine, Result}; -/// use wasmtime::component::{ResourceTable, Linker}; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// fn main() -> Result<()> { -/// let engine = Engine::default(); -/// -/// let mut linker = Linker::::new(&engine); -/// wasmtime_wasi_http::add_to_linker_async(&mut linker)?; -/// // ... add any further functionality to `linker` if desired ... -/// -/// Ok(()) -/// } -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// ``` -pub fn add_to_linker_async(l: &mut wasmtime::component::Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + wasmtime_wasi::WasiView + 'static, -{ - wasmtime_wasi::p2::add_to_linker_proxy_interfaces_async(l)?; - add_only_http_to_linker_async(l) -} - -/// A slimmed down version of [`add_to_linker_async`] which only adds -/// `wasi:http` interfaces to the linker. -/// -/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_async`] for -/// example to avoid re-adding the same interfaces twice. -pub fn add_only_http_to_linker_async( - l: &mut wasmtime::component::Linker, -) -> wasmtime::Result<()> -where - T: WasiHttpView + 'static, -{ - let options = crate::bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - crate::bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { - WasiHttpImpl(x) - })?; - crate::bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; - - Ok(()) -} - -struct WasiHttp(T); - -impl HasData for WasiHttp { - type Data<'a> = WasiHttpImpl<&'a mut T>; -} - -/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. -/// -/// This function will add the `sync` variant of all interfaces into the -/// `Linker` provided. For embeddings with async support see -/// [`add_to_linker_async`] instead. -/// -/// # Example -/// -/// ``` -/// use wasmtime::{Engine, Result, Config}; -/// use wasmtime::component::{ResourceTable, Linker}; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// fn main() -> Result<()> { -/// let config = Config::default(); -/// let engine = Engine::new(&config)?; -/// -/// let mut linker = Linker::::new(&engine); -/// wasmtime_wasi_http::add_to_linker_sync(&mut linker)?; -/// // ... add any further functionality to `linker` if desired ... -/// -/// Ok(()) -/// } -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// ``` -pub fn add_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + wasmtime_wasi::WasiView + 'static, -{ - wasmtime_wasi::p2::add_to_linker_proxy_interfaces_sync(l)?; - add_only_http_to_linker_sync(l) -} - -/// A slimmed down version of [`add_to_linker_sync`] which only adds -/// `wasi:http` interfaces to the linker. -/// -/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_sync`] for -/// example to avoid re-adding the same interfaces twice. -pub fn add_only_http_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + 'static, -{ - let options = crate::bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - crate::bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { - WasiHttpImpl(x) - })?; - crate::bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; - - Ok(()) -} +pub use ctx::*; /// Extract the `Content-Length` header value from a [`http::HeaderMap`], returning `None` if it's not /// present. This function will return `Err` if it's not possible to parse the `Content-Length` /// header. +#[cfg(any(feature = "p2", feature = "p3"))] fn get_content_length(headers: &http::HeaderMap) -> wasmtime::Result> { - let Some(v) = headers.get(CONTENT_LENGTH) else { + let Some(v) = headers.get(header::CONTENT_LENGTH) else { return Ok(None); }; let v = v.to_str()?; let v = v.parse()?; Ok(Some(v)) } + +/// Set of [http::header::HeaderName], that are forbidden by default +/// for requests and responses originating in the guest. +pub const DEFAULT_FORBIDDEN_HEADERS: [HeaderName; 9] = [ + header::CONNECTION, + HeaderName::from_static("keep-alive"), + header::PROXY_AUTHENTICATE, + header::PROXY_AUTHORIZATION, + HeaderName::from_static("proxy-connection"), + header::TRANSFER_ENCODING, + header::UPGRADE, + header::HOST, + HeaderName::from_static("http2-settings"), +]; diff --git a/crates/wasi-http/src/bindings.rs b/crates/wasi-http/src/p2/bindings.rs similarity index 93% rename from crates/wasi-http/src/bindings.rs rename to crates/wasi-http/src/p2/bindings.rs index bec9402a8775..e870ea033042 100644 --- a/crates/wasi-http/src/bindings.rs +++ b/crates/wasi-http/src/p2/bindings.rs @@ -2,8 +2,8 @@ #[expect(missing_docs, reason = "bindgen-generated code")] mod generated { - use crate::body; - use crate::types; + use crate::p2::body; + use crate::p2::types; wasmtime::component::bindgen!({ path: "wit", @@ -30,7 +30,7 @@ mod generated { "wasi:http/types.request-options": types::HostRequestOptions, }, trappable_error_type: { - "wasi:http/types.error-code" => crate::HttpError, + "wasi:http/types.error-code" => crate::p2::HttpError, }, }); } @@ -52,7 +52,7 @@ pub mod sync { imports: { default: tracing }, with: { // http is in this crate - "wasi:http": crate::bindings::http, + "wasi:http": crate::p2::bindings::http, // sync requires the wrapper in the wasmtime_wasi crate, in // order to have in_tokio "wasi:io": wasmtime_wasi::p2::bindings::sync::io, diff --git a/crates/wasi-http/src/body.rs b/crates/wasi-http/src/p2/body.rs similarity index 99% rename from crates/wasi-http/src/body.rs rename to crates/wasi-http/src/p2/body.rs index 000bd567c8c7..5d8c929b4278 100644 --- a/crates/wasi-http/src/body.rs +++ b/crates/wasi-http/src/p2/body.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/types` interface's various body types. -use crate::bindings::http::types; -use crate::types::FieldMap; +use crate::p2::bindings::http::types; +use crate::p2::types::FieldMap; use bytes::Bytes; use http_body::{Body, Frame}; use http_body_util::BodyExt; diff --git a/crates/wasi-http/src/error.rs b/crates/wasi-http/src/p2/error.rs similarity index 96% rename from crates/wasi-http/src/error.rs rename to crates/wasi-http/src/p2/error.rs index 4279bd8483f3..9bc1ebd6f057 100644 --- a/crates/wasi-http/src/error.rs +++ b/crates/wasi-http/src/p2/error.rs @@ -1,4 +1,4 @@ -use crate::bindings::http::types::ErrorCode; +use crate::p2::bindings::http::types::ErrorCode; use std::error::Error; use std::fmt; use wasmtime::component::ResourceTableError; @@ -60,7 +60,7 @@ impl Error for HttpError {} #[cfg(feature = "default-send-request")] pub(crate) fn dns_error(rcode: String, info_code: u16) -> ErrorCode { - ErrorCode::DnsError(crate::bindings::http::types::DnsErrorPayload { + ErrorCode::DnsError(crate::p2::bindings::http::types::DnsErrorPayload { rcode: Some(rcode), info_code: Some(info_code), }) diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/p2/http_impl.rs similarity index 96% rename from crates/wasi-http/src/http_impl.rs rename to crates/wasi-http/src/p2/http_impl.rs index 903072cd43aa..e0b46816b436 100644 --- a/crates/wasi-http/src/http_impl.rs +++ b/crates/wasi-http/src/p2/http_impl.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/outgoing-handler` interface. -use crate::{ - WasiHttpImpl, WasiHttpView, +use crate::p2::{ + HttpResult, WasiHttpImpl, WasiHttpView, bindings::http::{ outgoing_handler, types::{self, Scheme}, @@ -23,7 +23,7 @@ where &mut self, request_id: Resource, options: Option>, - ) -> crate::HttpResult> { + ) -> HttpResult> { let opts = options.and_then(|opts| self.table().get(&opts).ok()); let connect_timeout = opts diff --git a/crates/wasi-http/src/p2/mod.rs b/crates/wasi-http/src/p2/mod.rs new file mode 100644 index 000000000000..8f502dddae74 --- /dev/null +++ b/crates/wasi-http/src/p2/mod.rs @@ -0,0 +1,376 @@ +//! # Wasmtime's WASI HTTPp2 Implementation +//! +//! This module is Wasmtime's host implementation of the `wasi:http` package as +//! part of WASIp2. This crate's implementation is primarily built on top of +//! [`hyper`] and [`tokio`]. +//! +//! # WASI HTTP Interfaces +//! +//! This crate contains implementations of the following interfaces: +//! +//! * [`wasi:http/incoming-handler`] +//! * [`wasi:http/outgoing-handler`] +//! * [`wasi:http/types`] +//! +//! The crate also contains an implementation of the [`wasi:http/proxy`] world. +//! +//! [`wasi:http/proxy`]: crate::bindings::Proxy +//! [`wasi:http/outgoing-handler`]: crate::bindings::http::outgoing_handler::Host +//! [`wasi:http/types`]: crate::bindings::http::types::Host +//! [`wasi:http/incoming-handler`]: crate::bindings::exports::wasi::http::incoming_handler::Guest +//! +//! This crate is very similar to [`wasmtime_wasi`] in the it uses the +//! `bindgen!` macro in Wasmtime to generate bindings to interfaces. Bindings +//! are located in the [`bindings`] module. +//! +//! # The `WasiHttpView` trait +//! +//! All `bindgen!`-generated `Host` traits are implemented in terms of a +//! [`WasiHttpView`] trait which provides basic access to [`WasiHttpCtx`], +//! configuration for WASI HTTP, and a [`wasmtime_wasi::ResourceTable`], the +//! state for all host-defined component model resources. +//! +//! The [`WasiHttpView`] trait additionally offers a few other configuration +//! methods such as [`WasiHttpView::send_request`] to customize how outgoing +//! HTTP requests are handled. +//! +//! # Async and Sync +//! +//! There are both asynchronous and synchronous bindings in this crate. For +//! example [`add_to_linker_async`] is for asynchronous embedders and +//! [`add_to_linker_sync`] is for synchronous embedders. Note that under the +//! hood both versions are implemented with `async` on top of [`tokio`]. +//! +//! # Examples +//! +//! Usage of this crate is done through a few steps to get everything hooked up: +//! +//! 1. First implement [`WasiHttpView`] for your type which is the `T` in +//! [`wasmtime::Store`]. +//! 2. Add WASI HTTP interfaces to a [`wasmtime::component::Linker`]. There +//! are a few options of how to do this: +//! * Use [`add_to_linker_async`] to bundle all interfaces in +//! `wasi:http/proxy` together +//! * Use [`add_only_http_to_linker_async`] to add only HTTP interfaces but +//! no others. This is useful when working with +//! [`wasmtime_wasi::p2::add_to_linker_async`] for example. +//! * Add individual interfaces such as with the +//! [`bindings::http::outgoing_handler::add_to_linker`] function. +//! 3. Use [`ProxyPre`](bindings::ProxyPre) to pre-instantiate a component +//! before serving requests. +//! 4. When serving requests use +//! [`ProxyPre::instantiate_async`](bindings::ProxyPre::instantiate_async) +//! to create instances and handle HTTP requests. +//! +//! A standalone example of doing all this looks like: +//! +//! ```no_run +//! use wasmtime::bail; +//! use hyper::server::conn::http1; +//! use std::sync::Arc; +//! use tokio::net::TcpListener; +//! use wasmtime::component::{Component, Linker, ResourceTable}; +//! use wasmtime::{Engine, Result, Store}; +//! use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +//! use wasmtime_wasi_http::p2::bindings::ProxyPre; +//! use wasmtime_wasi_http::p2::bindings::http::types::Scheme; +//! use wasmtime_wasi_http::p2::body::HyperOutgoingBody; +//! use wasmtime_wasi_http::io::TokioIo; +//! use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let component = std::env::args().nth(1).unwrap(); +//! +//! // Prepare the `Engine` for Wasmtime +//! let engine = Engine::default(); +//! +//! // Compile the component on the command line to machine code +//! let component = Component::from_file(&engine, &component)?; +//! +//! // Prepare the `ProxyPre` which is a pre-instantiated version of the +//! // component that we have. This will make per-request instantiation +//! // much quicker. +//! let mut linker = Linker::new(&engine); +//! wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; +//! wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?; +//! let pre = ProxyPre::new(linker.instantiate_pre(&component)?)?; +//! +//! // Prepare our server state and start listening for connections. +//! let server = Arc::new(MyServer { pre }); +//! let listener = TcpListener::bind("127.0.0.1:8000").await?; +//! println!("Listening on {}", listener.local_addr()?); +//! +//! loop { +//! // Accept a TCP connection and serve all of its requests in a separate +//! // tokio task. Note that for now this only works with HTTP/1.1. +//! let (client, addr) = listener.accept().await?; +//! println!("serving new client from {addr}"); +//! +//! let server = server.clone(); +//! tokio::task::spawn(async move { +//! if let Err(e) = http1::Builder::new() +//! .keep_alive(true) +//! .serve_connection( +//! TokioIo::new(client), +//! hyper::service::service_fn(move |req| { +//! let server = server.clone(); +//! async move { server.handle_request(req).await } +//! }), +//! ) +//! .await +//! { +//! eprintln!("error serving client[{addr}]: {e:?}"); +//! } +//! }); +//! } +//! } +//! +//! struct MyServer { +//! pre: ProxyPre, +//! } +//! +//! impl MyServer { +//! async fn handle_request( +//! &self, +//! req: hyper::Request, +//! ) -> Result> { +//! // Create per-http-request state within a `Store` and prepare the +//! // initial resources passed to the `handle` function. +//! let mut store = Store::new( +//! self.pre.engine(), +//! MyClientState { +//! table: ResourceTable::new(), +//! wasi: WasiCtx::builder().inherit_stdio().build(), +//! http: WasiHttpCtx::new(), +//! }, +//! ); +//! let (sender, receiver) = tokio::sync::oneshot::channel(); +//! let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; +//! let out = store.data_mut().new_response_outparam(sender)?; +//! let pre = self.pre.clone(); +//! +//! // Run the http request itself in a separate task so the task can +//! // optionally continue to execute beyond after the initial +//! // headers/response code are sent. +//! let task = tokio::task::spawn(async move { +//! let proxy = pre.instantiate_async(&mut store).await?; +//! +//! if let Err(e) = proxy +//! .wasi_http_incoming_handler() +//! .call_handle(store, req, out) +//! .await +//! { +//! return Err(e); +//! } +//! +//! Ok(()) +//! }); +//! +//! match receiver.await { +//! // If the client calls `response-outparam::set` then one of these +//! // methods will be called. +//! Ok(Ok(resp)) => Ok(resp), +//! Ok(Err(e)) => Err(e.into()), +//! +//! // Otherwise the `sender` will get dropped along with the `Store` +//! // meaning that the oneshot will get disconnected and here we can +//! // inspect the `task` result to see what happened +//! Err(_) => { +//! let e = match task.await { +//! Ok(Ok(())) => { +//! bail!("guest never invoked `response-outparam::set` method") +//! } +//! Ok(Err(e)) => e, +//! Err(e) => e.into(), +//! }; +//! return Err(e.context("guest never invoked `response-outparam::set` method")); +//! } +//! } +//! } +//! } +//! +//! struct MyClientState { +//! wasi: WasiCtx, +//! http: WasiHttpCtx, +//! table: ResourceTable, +//! } +//! +//! impl WasiView for MyClientState { +//! fn ctx(&mut self) -> WasiCtxView<'_> { +//! WasiCtxView { ctx: &mut self.wasi, table: &mut self.table } +//! } +//! } +//! +//! impl WasiHttpView for MyClientState { +//! fn ctx(&mut self) -> &mut WasiHttpCtx { +//! &mut self.http +//! } +//! +//! fn table(&mut self) -> &mut ResourceTable { +//! &mut self.table +//! } +//! } +//! ``` + +mod error; +mod http_impl; +mod types_impl; + +pub mod body; +pub mod types; + +pub mod bindings; + +pub use self::error::{ + HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, +}; +#[doc(inline)] +pub use self::types::{ + DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpImpl, + WasiHttpView, +}; +use wasmtime::component::{HasData, Linker}; + +/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. +/// +/// This function will add the `async` variant of all interfaces into the +/// `Linker` provided. For embeddings with async support disabled see +/// [`add_to_linker_sync`] instead. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; +/// +/// fn main() -> Result<()> { +/// let engine = Engine::default(); +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_http::p2::add_to_linker_async(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } +/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_async(l: &mut wasmtime::component::Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + wasmtime_wasi::WasiView + 'static, +{ + wasmtime_wasi::p2::add_to_linker_proxy_interfaces_async(l)?; + add_only_http_to_linker_async(l) +} + +/// A slimmed down version of [`add_to_linker_async`] which only adds +/// `wasi:http` interfaces to the linker. +/// +/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_async`] for +/// example to avoid re-adding the same interfaces twice. +pub fn add_only_http_to_linker_async( + l: &mut wasmtime::component::Linker, +) -> wasmtime::Result<()> +where + T: WasiHttpView + 'static, +{ + let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. + bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| WasiHttpImpl(x))?; + bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { + WasiHttpImpl(x) + })?; + + Ok(()) +} + +struct WasiHttp(T); + +impl HasData for WasiHttp { + type Data<'a> = WasiHttpImpl<&'a mut T>; +} + +/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. +/// +/// This function will add the `sync` variant of all interfaces into the +/// `Linker` provided. For embeddings with async support see +/// [`add_to_linker_async`] instead. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; +/// +/// fn main() -> Result<()> { +/// let config = Config::default(); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_http::p2::add_to_linker_sync(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// impl WasiHttpView for MyState { +/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } +/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// } +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + wasmtime_wasi::WasiView + 'static, +{ + wasmtime_wasi::p2::add_to_linker_proxy_interfaces_sync(l)?; + add_only_http_to_linker_sync(l) +} + +/// A slimmed down version of [`add_to_linker_sync`] which only adds +/// `wasi:http` interfaces to the linker. +/// +/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_sync`] for +/// example to avoid re-adding the same interfaces twice. +pub fn add_only_http_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + 'static, +{ + let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. + bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { + WasiHttpImpl(x) + })?; + bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { + WasiHttpImpl(x) + })?; + + Ok(()) +} diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/p2/types.rs similarity index 92% rename from crates/wasi-http/src/types.rs rename to crates/wasi-http/src/p2/types.rs index d120e0555410..88fcf691872e 100644 --- a/crates/wasi-http/src/types.rs +++ b/crates/wasi-http/src/p2/types.rs @@ -1,10 +1,12 @@ //! Implements the base structure (i.e. [WasiHttpCtx]) that will provide the //! implementation of the wasi-http API. -use crate::{ +use crate::p2::{ + HttpResult, bindings::http::types::{self, ErrorCode, Method, Scheme}, body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody}, }; +use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; use bytes::Bytes; use http::header::{HeaderMap, HeaderName, HeaderValue}; use http_body_util::BodyExt; @@ -20,47 +22,11 @@ use wasmtime_wasi::runtime::AbortOnDropJoinHandle; #[cfg(feature = "default-send-request")] use { crate::io::TokioIo, - crate::{error::dns_error, hyper_request_error}, + crate::p2::{error::dns_error, hyper_request_error}, tokio::net::TcpStream, tokio::time::timeout, }; -/// Default maximum size for the contents of a fields resource. -/// -/// Typically, HTTP proxies limit headers to 8k. This number is higher than that -/// because it not only includes the wire-size of headers but it additionally -/// includes factors for the in-memory representation of `HeaderMap`. This is in -/// theory high enough that no one runs into it but low enough such that a -/// completely full `HeaderMap` doesn't break the bank in terms of memory -/// consumption. -const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024; - -/// Capture the state necessary for use in the wasi-http API implementation. -#[derive(Debug)] -pub struct WasiHttpCtx { - pub(crate) field_size_limit: usize, -} - -impl WasiHttpCtx { - /// Create a new context. - pub fn new() -> Self { - Self { - field_size_limit: DEFAULT_FIELD_SIZE_LIMIT, - } - } - - /// Set the maximum size for any fields resources created by this context. - /// - /// The limit specified here is roughly a byte limit for the size of the - /// in-memory representation of headers. This means that the limit needs to - /// be larger than the literal representation of headers on the wire to - /// account for in-memory Rust-side data structures representing the header - /// names/values/etc. - pub fn set_field_size_limit(&mut self, limit: usize) { - self.field_size_limit = limit; - } -} - /// A trait which provides internal WASI HTTP state. /// /// # Example @@ -68,7 +34,7 @@ impl WasiHttpCtx { /// ``` /// use wasmtime::component::ResourceTable; /// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; /// /// struct MyState { /// ctx: WasiCtx, @@ -151,7 +117,7 @@ pub trait WasiHttpView { &mut self, request: hyper::Request, config: OutgoingRequestConfig, - ) -> crate::HttpResult { + ) -> HttpResult { Ok(default_send_request(request, config)) } @@ -161,7 +127,7 @@ pub trait WasiHttpView { &mut self, request: hyper::Request, config: OutgoingRequestConfig, - ) -> crate::HttpResult; + ) -> HttpResult; /// Whether a given header should be considered forbidden and not allowed. fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { @@ -209,7 +175,7 @@ impl WasiHttpView for &mut T { &mut self, request: hyper::Request, config: OutgoingRequestConfig, - ) -> crate::HttpResult { + ) -> HttpResult { T::send_request(self, request, config) } @@ -248,7 +214,7 @@ impl WasiHttpView for Box { &mut self, request: hyper::Request, config: OutgoingRequestConfig, - ) -> crate::HttpResult { + ) -> HttpResult { T::send_request(self, request, config) } @@ -302,7 +268,7 @@ impl WasiHttpView for WasiHttpImpl { &mut self, request: hyper::Request, config: OutgoingRequestConfig, - ) -> crate::HttpResult { + ) -> HttpResult { self.0.send_request(request, config) } @@ -319,20 +285,6 @@ impl WasiHttpView for WasiHttpImpl { } } -/// Set of [http::header::HeaderName], that are forbidden by default -/// for requests and responses originating in the guest. -pub const DEFAULT_FORBIDDEN_HEADERS: [http::header::HeaderName; 9] = [ - hyper::header::CONNECTION, - HeaderName::from_static("keep-alive"), - hyper::header::PROXY_AUTHENTICATE, - hyper::header::PROXY_AUTHORIZATION, - HeaderName::from_static("proxy-connection"), - hyper::header::TRANSFER_ENCODING, - hyper::header::UPGRADE, - hyper::header::HOST, - HeaderName::from_static("http2-settings"), -]; - /// Removes forbidden headers from a [`FieldMap`]. pub(crate) fn remove_forbidden_headers(view: &mut dyn WasiHttpView, headers: &mut FieldMap) { let forbidden_keys = Vec::from_iter(headers.as_ref().keys().filter_map(|name| { diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/p2/types_impl.rs similarity index 95% rename from crates/wasi-http/src/types_impl.rs rename to crates/wasi-http/src/p2/types_impl.rs index 45db0df7d8ab..d0b468e1f53a 100644 --- a/crates/wasi-http/src/types_impl.rs +++ b/crates/wasi-http/src/p2/types_impl.rs @@ -1,13 +1,14 @@ //! Implementation for the `wasi:http/types` interface. -use crate::bindings::http::types::{self, Headers, Method, Scheme, StatusCode, Trailers}; -use crate::body::{HostFutureTrailers, HostIncomingBody, HostOutgoingBody, StreamContext}; -use crate::types::{ +use crate::get_content_length; +use crate::p2::bindings::http::types::{self, Headers, Method, Scheme, StatusCode, Trailers}; +use crate::p2::body::{HostFutureTrailers, HostIncomingBody, HostOutgoingBody, StreamContext}; +use crate::p2::types::{ FieldMap, FieldSizeLimitError, HostFields, HostFutureIncomingResponse, HostIncomingRequest, HostIncomingResponse, HostOutgoingRequest, HostOutgoingResponse, HostResponseOutparam, remove_forbidden_headers, }; -use crate::{HttpError, HttpResult, WasiHttpImpl, WasiHttpView, get_content_length}; +use crate::p2::{HttpError, HttpResult, WasiHttpImpl, WasiHttpView}; use std::any::Any; use std::str::FromStr; use wasmtime::bail; @@ -15,11 +16,11 @@ use wasmtime::component::{Resource, ResourceTable, ResourceTableError}; use wasmtime::{error::Context as _, format_err}; use wasmtime_wasi::p2::{DynInputStream, DynOutputStream, DynPollable}; -impl crate::bindings::http::types::Host for WasiHttpImpl +impl types::Host for WasiHttpImpl where T: WasiHttpView, { - fn convert_error_code(&mut self, err: crate::HttpError) -> wasmtime::Result { + fn convert_error_code(&mut self, err: HttpError) -> wasmtime::Result { err.downcast() } @@ -77,7 +78,7 @@ fn get_fields_mut<'a>( } } -impl crate::bindings::http::types::HostFields for WasiHttpImpl +impl types::HostFields for WasiHttpImpl where T: WasiHttpView, { @@ -286,7 +287,7 @@ where } } -impl crate::bindings::http::types::HostIncomingRequest for WasiHttpImpl +impl types::HostIncomingRequest for WasiHttpImpl where T: WasiHttpView, { @@ -355,7 +356,7 @@ where } } -impl crate::bindings::http::types::HostOutgoingRequest for WasiHttpImpl +impl types::HostOutgoingRequest for WasiHttpImpl where T: WasiHttpView, { @@ -542,7 +543,7 @@ where } } -impl crate::bindings::http::types::HostResponseOutparam for WasiHttpImpl +impl types::HostResponseOutparam for WasiHttpImpl where T: WasiHttpView, { @@ -579,7 +580,7 @@ where } } -impl crate::bindings::http::types::HostIncomingResponse for WasiHttpImpl +impl types::HostIncomingResponse for WasiHttpImpl where T: WasiHttpView, { @@ -643,7 +644,7 @@ where } } -impl crate::bindings::http::types::HostFutureTrailers for WasiHttpImpl +impl types::HostFutureTrailers for WasiHttpImpl where T: WasiHttpView, { @@ -693,7 +694,7 @@ where } } -impl crate::bindings::http::types::HostIncomingBody for WasiHttpImpl +impl types::HostIncomingBody for WasiHttpImpl where T: WasiHttpView, { @@ -727,7 +728,7 @@ where } } -impl crate::bindings::http::types::HostOutgoingResponse for WasiHttpImpl +impl types::HostOutgoingResponse for WasiHttpImpl where T: WasiHttpView, { @@ -822,7 +823,7 @@ where } } -impl crate::bindings::http::types::HostFutureIncomingResponse for WasiHttpImpl +impl types::HostFutureIncomingResponse for WasiHttpImpl where T: WasiHttpView, { @@ -887,7 +888,7 @@ where } } -impl crate::bindings::http::types::HostOutgoingBody for WasiHttpImpl +impl types::HostOutgoingBody for WasiHttpImpl where T: WasiHttpView, { @@ -908,7 +909,7 @@ where &mut self, id: Resource, ts: Option>, - ) -> crate::HttpResult<()> { + ) -> HttpResult<()> { let body = self.table().delete(id)?; let ts = if let Some(ts) = ts { @@ -927,7 +928,7 @@ where } } -impl crate::bindings::http::types::HostRequestOptions for WasiHttpImpl +impl types::HostRequestOptions for WasiHttpImpl where T: WasiHttpView, { diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index d8af50b68533..59048fdca154 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -21,8 +21,8 @@ pub use request::default_send_request; pub use request::{Request, RequestOptions}; pub use response::Response; +use crate::DEFAULT_FORBIDDEN_HEADERS; use crate::p3::bindings::http::types::ErrorCode; -use crate::types::DEFAULT_FORBIDDEN_HEADERS; use bindings::http::{client, types}; use bytes::Bytes; use core::ops::Deref; diff --git a/crates/wasi-http/tests/all/p2.rs b/crates/wasi-http/tests/all/p2.rs index 9d070789d027..cee6dd3e860d 100644 --- a/crates/wasi-http/tests/all/p2.rs +++ b/crates/wasi-http/tests/all/p2.rs @@ -15,11 +15,12 @@ use wasmtime::{ }; use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView, p2::pipe::MemoryOutputPipe}; use wasmtime_wasi_http::{ - HttpResult, WasiHttpCtx, WasiHttpView, - bindings::http::types::{ErrorCode, Scheme}, - body::HyperOutgoingBody, + WasiHttpCtx, io::TokioIo, - types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + p2::bindings::http::types::{ErrorCode, Scheme}, + p2::body::HyperOutgoingBody, + p2::types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + p2::{HttpResult, WasiHttpView}, }; type RequestSender = Arc< @@ -75,7 +76,7 @@ impl WasiHttpView for Ctx { } fn is_forbidden_header(&mut self, name: &hyper::header::HeaderName) -> bool { - types::DEFAULT_FORBIDDEN_HEADERS.contains(name) + wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS.contains(name) || name.as_str() == "custom-forbidden-header" } } @@ -158,9 +159,9 @@ async fn run_wasi_http( let mut store = Store::new(&engine, ctx); let mut linker = Linker::new(&engine); - wasmtime_wasi_http::add_to_linker_async(&mut linker).context("add crate to linker")?; + wasmtime_wasi_http::p2::add_to_linker_async(&mut linker).context("add crate to linker")?; let proxy = - wasmtime_wasi_http::bindings::Proxy::instantiate_async(&mut store, &component, &linker) + wasmtime_wasi_http::p2::bindings::Proxy::instantiate_async(&mut store, &component, &linker) .await .context("instantiate proxy")?; @@ -298,7 +299,7 @@ async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> { let response = handle(request.into_parts().0).map(|resp| { Ok(IncomingResponse { resp: resp.map(|body| { - body.map_err(wasmtime_wasi_http::hyper_response_error) + body.map_err(wasmtime_wasi_http::p2::hyper_response_error) .boxed_unsync() }), worker: None, diff --git a/crates/wasi-http/tests/all/p2/async_.rs b/crates/wasi-http/tests/all/p2/async_.rs index f5a8674255e5..176a713cd67b 100644 --- a/crates/wasi-http/tests/all/p2/async_.rs +++ b/crates/wasi-http/tests/all/p2/async_.rs @@ -12,7 +12,7 @@ async fn run(path: &str, server: &Server) -> Result<()> { let mut store = store(&engine, server); let mut linker = Linker::new(&engine); wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; - wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?; let command = Command::instantiate_async(&mut store, &component, &linker).await?; let result = command.wasi_cli_run().call_run(&mut store).await?; result.map_err(|()| wasmtime::format_err!("run returned an error")) diff --git a/crates/wasi-http/tests/all/p2/sync.rs b/crates/wasi-http/tests/all/p2/sync.rs index 5ac57caa427e..a715c4ba31d2 100644 --- a/crates/wasi-http/tests/all/p2/sync.rs +++ b/crates/wasi-http/tests/all/p2/sync.rs @@ -12,7 +12,7 @@ fn run(path: &str, server: &Server) -> Result<()> { let mut store = store(&engine, server); let mut linker = Linker::new(&engine); wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?; - wasmtime_wasi_http::add_only_http_to_linker_sync(&mut linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_sync(&mut linker)?; let command = Command::instantiate(&mut store, &component, &linker)?; let result = command.wasi_cli_run().call_run(&mut store)?; result.map_err(|()| wasmtime::format_err!("run returned an error")) diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs index 9d11a50ecd39..d8e161385b86 100644 --- a/crates/wasi-http/tests/all/p3/mod.rs +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -19,12 +19,12 @@ use wasmtime::component::{Component, Linker, ResourceTable}; use wasmtime::{Result, Store, ToWasmtimeResult as _, error::Context as _, format_err}; use wasmtime_wasi::p3::bindings::Command; use wasmtime_wasi::{TrappableError, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; +use wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS; use wasmtime_wasi_http::p3::bindings::Service; use wasmtime_wasi_http::p3::bindings::http::types::ErrorCode; use wasmtime_wasi_http::p3::{ self, Request, RequestOptions, WasiHttpCtx, WasiHttpCtxView, WasiHttpView, }; -use wasmtime_wasi_http::types::DEFAULT_FORBIDDEN_HEADERS; foreach_p3_http!(assert_test_exists); diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index ec65711df966..ee2d4a95db80 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -15,6 +15,9 @@ include = ["src/**/*", "README.md", "LICENSE", "witx/*", "wit/**/*", "tests/*"] [lints] workspace = true +[package.metadata.docs.rs] +all-features = true + [dependencies] wasmtime = { workspace = true, features = ["runtime", "std"] } wasmtime-wasi-io = { workspace = true, features = ["std"] } diff --git a/src/commands/run.rs b/src/commands/run.rs index 91aa2bf2435c..a10211bbd816 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -21,9 +21,7 @@ use wasmtime_wasi::{WasiCtxView, WasiView}; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; #[cfg(feature = "wasi-http")] -use wasmtime_wasi_http::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, -}; +use wasmtime_wasi_http::WasiHttpCtx; #[cfg(feature = "wasi-keyvalue")] use wasmtime_wasi_keyvalue::{WasiKeyValue, WasiKeyValueCtx, WasiKeyValueCtxBuilder}; #[cfg(feature = "wasi-nn")] @@ -1050,7 +1048,7 @@ impl RunCommand { bail!("Cannot enable wasi-http for core wasm modules"); } CliLinker::Component(linker) => { - wasmtime_wasi_http::add_only_http_to_linker_sync(linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; @@ -1258,7 +1256,7 @@ impl WasiView for Host { } #[cfg(feature = "wasi-http")] -impl wasmtime_wasi_http::types::WasiHttpView for Host { +impl wasmtime_wasi_http::p2::WasiHttpView for Host { fn ctx(&mut self) -> &mut WasiHttpCtx { let ctx = self.wasi_http.as_mut().unwrap(); Arc::get_mut(ctx).expect("wasmtime_wasi is not compatible with threads") @@ -1270,12 +1268,12 @@ impl wasmtime_wasi_http::types::WasiHttpView for Host { fn outgoing_body_buffer_chunks(&mut self) -> usize { self.wasi_http_outgoing_body_buffer_chunks - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) } fn outgoing_body_chunk_size(&mut self) -> usize { self.wasi_http_outgoing_body_chunk_size - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE) } } diff --git a/src/commands/serve.rs b/src/commands/serve.rs index cacf7110faa9..bb53c52b2ce3 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -31,10 +31,7 @@ use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; use wasmtime_wasi_http::handler::p2::bindings as p2; use wasmtime_wasi_http::handler::{HandlerState, Proxy, ProxyHandler, ProxyPre, StoreBundle}; use wasmtime_wasi_http::io::TokioIo; -use wasmtime_wasi_http::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, - WasiHttpView, -}; +use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; @@ -91,12 +88,12 @@ impl WasiHttpView for Host { fn outgoing_body_buffer_chunks(&mut self) -> usize { self.http_outgoing_body_buffer_chunks - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) } fn outgoing_body_chunk_size(&mut self) -> usize { self.http_outgoing_body_chunk_size - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE) } } @@ -337,13 +334,13 @@ impl ServeCommand { // uses. if cli == Some(true) { self.run.add_wasmtime_wasi_to_linker(linker)?; - wasmtime_wasi_http::add_only_http_to_linker_async(linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; } } else { - wasmtime_wasi_http::add_to_linker_async(linker)?; + wasmtime_wasi_http::p2::add_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; @@ -854,7 +851,7 @@ async fn handle_request( // `wasmtime::Error`. type P2Response = Result< - hyper::Response, + hyper::Response, p2::http::types::ErrorCode, >; type P3Response = hyper::Response>; From 9781746e26d7114f81cc87abce633c79ceaac1f0 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 13:12:28 -0700 Subject: [PATCH 2/7] Refactor WASIp2 `wasi:http` implementation This commit reorganizes and refactors the WASIp2 implementation of `wasi:http` to look more like other `wasmtime-wasi`-style interfaces. Specifically the old `WasiHttpImpl` structure is removed in favor of as `WasiHttpCtxView<'_>` type that is used to implement bindgen-generated `Host` traits. This necessitated reorganizing the methods of the previous `WasiHttpView` trait like so: * The `WasiHttpView` trait is renamed to `WasiHttpHooks` to make space for a new `WasiHttpView` which behaves like `WasiView`, for example. * The `ctx` and `table` methods of `WasiHttpHooks` were removed since they'll be fields in `WasiHttpCtxView`. * Helper methods for WASIp2 were moved to methods on `WasiHttpCtxView` instead of default methods on `WasiHttpHooks`. With these changes in place the WASIp3 organization was also updated slightly as well. Notably WASIp3 now contains a reference to the crate's `WasiHttpCtx` structure (which has field limits for example). WASIp3's previous `WasiHttpCtx` trait is now renamed to `WasiHttpHooks` as well. This means that there are two `WasiHttpHooks` traits right now, one for WASIp2 and one for WASIp3. In the future I would like to unify these two but that will require some more work around the default `send_request`. A final note here is that the `WasiHttpHooks` trait previously, and continues to be, optional for embedders to implement. Default functions are provided as `wasmtime_wasi_http::{p2, p3}::default_hooks`. Additionally there's a `Default for &mut dyn WasiHttpHooks` implementation, too. With all that outlined: the motivation for this change is to bring the WASIp2 and WASIp3 implementations of `wasi:http` closer together. This is inspired by refactorings I was doing for #12674 to apply the same header limitations for WASIp3 as is done for WASIp2. Prior to this change there were a number of differences such as WASIp3 not having `crate::WasiHttpCtx` around, WASIp2 having a different organization of structures/borrows, etc. The goal is to bring the two implementations closer in line with each other to make refactoring across them more consistent and easier. --- crates/wasi-http/src/ctx.rs | 8 +- crates/wasi-http/src/p2/http_impl.rs | 15 +- crates/wasi-http/src/p2/mod.rs | 401 ++++++++++++++++++-- crates/wasi-http/src/p2/types.rs | 474 ++---------------------- crates/wasi-http/src/p2/types_impl.rs | 261 ++++++------- crates/wasi-http/src/p3/host/handler.rs | 2 +- crates/wasi-http/src/p3/host/types.rs | 10 +- crates/wasi-http/src/p3/mod.rs | 30 +- crates/wasi-http/src/p3/request.rs | 15 +- crates/wasi-http/tests/all/p2.rs | 38 +- crates/wasi-http/tests/all/p3/mod.rs | 15 +- src/commands/run.rs | 55 +-- src/commands/serve.rs | 44 +-- src/common.rs | 52 ++- 14 files changed, 676 insertions(+), 744 deletions(-) diff --git a/crates/wasi-http/src/ctx.rs b/crates/wasi-http/src/ctx.rs index bf4ff50672ef..0ae1a9678e22 100644 --- a/crates/wasi-http/src/ctx.rs +++ b/crates/wasi-http/src/ctx.rs @@ -9,7 +9,7 @@ const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024; /// Capture the state necessary for use in the wasi-http API implementation. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct WasiHttpCtx { pub(crate) field_size_limit: usize, } @@ -33,3 +33,9 @@ impl WasiHttpCtx { self.field_size_limit = limit; } } + +impl Default for WasiHttpCtx { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/wasi-http/src/p2/http_impl.rs b/crates/wasi-http/src/p2/http_impl.rs index e0b46816b436..45b4312044d9 100644 --- a/crates/wasi-http/src/p2/http_impl.rs +++ b/crates/wasi-http/src/p2/http_impl.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/outgoing-handler` interface. use crate::p2::{ - HttpResult, WasiHttpImpl, WasiHttpView, + HttpResult, WasiHttpCtxView, bindings::http::{ outgoing_handler, types::{self, Scheme}, @@ -15,16 +15,13 @@ use http_body_util::{BodyExt, Empty}; use hyper::Method; use wasmtime::component::Resource; -impl outgoing_handler::Host for WasiHttpImpl -where - T: WasiHttpView, -{ +impl outgoing_handler::Host for WasiHttpCtxView<'_> { fn handle( &mut self, request_id: Resource, options: Option>, ) -> HttpResult> { - let opts = options.and_then(|opts| self.table().get(&opts).ok()); + let opts = options.and_then(|opts| self.table.get(&opts).ok()); let connect_timeout = opts .and_then(|opts| opts.connect_timeout) @@ -38,7 +35,7 @@ where .and_then(|opts| opts.between_bytes_timeout) .unwrap_or(std::time::Duration::from_secs(600)); - let req = self.table().delete(request_id)?; + let req = self.table.delete(request_id)?; let mut builder = hyper::Request::builder(); builder = builder.method(match req.method { @@ -93,7 +90,7 @@ where .body(body) .map_err(|err| internal_error(err.to_string()))?; - let future = self.send_request( + let future = self.hooks.send_request( request, OutgoingRequestConfig { use_tls, @@ -103,6 +100,6 @@ where }, )?; - Ok(self.table().push(future)?) + Ok(self.table.push(future)?) } } diff --git a/crates/wasi-http/src/p2/mod.rs b/crates/wasi-http/src/p2/mod.rs index 8f502dddae74..e0bdfb9c743a 100644 --- a/crates/wasi-http/src/p2/mod.rs +++ b/crates/wasi-http/src/p2/mod.rs @@ -76,7 +76,7 @@ //! use wasmtime_wasi_http::p2::bindings::http::types::Scheme; //! use wasmtime_wasi_http::p2::body::HyperOutgoingBody; //! use wasmtime_wasi_http::io::TokioIo; -//! use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; +//! use wasmtime_wasi_http::{WasiHttpCtx, p2::{WasiHttpView, WasiHttpCtxView}}; //! //! #[tokio::main] //! async fn main() -> Result<()> { @@ -146,8 +146,8 @@ //! }, //! ); //! let (sender, receiver) = tokio::sync::oneshot::channel(); -//! let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; -//! let out = store.data_mut().new_response_outparam(sender)?; +//! let req = store.data_mut().http().new_incoming_request(Scheme::Http, req)?; +//! let out = store.data_mut().http().new_response_outparam(sender)?; //! let pre = self.pre.clone(); //! //! // Run the http request itself in a separate task so the task can @@ -203,34 +203,202 @@ //! } //! //! impl WasiHttpView for MyClientState { -//! fn ctx(&mut self) -> &mut WasiHttpCtx { -//! &mut self.http -//! } -//! -//! fn table(&mut self) -> &mut ResourceTable { -//! &mut self.table +//! fn http(&mut self) -> WasiHttpCtxView<'_> { +//! WasiHttpCtxView { +//! ctx: &mut self.http, +//! table: &mut self.table, +//! hooks: Default::default(), +//! } //! } //! } //! ``` +#[cfg(feature = "default-send-request")] +use self::bindings::http::types::ErrorCode; +use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; +use http::HeaderName; +use wasmtime::component::{HasData, Linker, ResourceTable}; + mod error; mod http_impl; mod types_impl; +pub mod bindings; pub mod body; pub mod types; -pub mod bindings; - pub use self::error::{ HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, }; -#[doc(inline)] -pub use self::types::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpImpl, - WasiHttpView, -}; -use wasmtime::component::{HasData, Linker}; + +/// A trait which provides hooks into internal WASI HTTP operations. +/// +/// # Example +/// +/// ``` +/// use wasmtime::component::ResourceTable; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// +/// impl MyState { +/// fn new() -> MyState { +/// let mut wasi = WasiCtx::builder(); +/// wasi.arg("./foo.wasm"); +/// wasi.arg("--help"); +/// wasi.env("FOO", "bar"); +/// +/// MyState { +/// ctx: wasi.build(), +/// table: ResourceTable::new(), +/// http_ctx: WasiHttpCtx::new(), +/// } +/// } +/// } +/// ``` +pub trait WasiHttpHooks { + /// Send an outgoing request. + #[cfg(feature = "default-send-request")] + fn send_request( + &mut self, + request: hyper::Request, + config: types::OutgoingRequestConfig, + ) -> HttpResult { + Ok(default_send_request(request, config)) + } + + /// Send an outgoing request. + #[cfg(not(feature = "default-send-request"))] + fn send_request( + &mut self, + request: hyper::Request, + config: types::OutgoingRequestConfig, + ) -> HttpResult; + + /// Whether a given header should be considered forbidden and not allowed. + fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { + DEFAULT_FORBIDDEN_HEADERS.contains(name) + } + + /// Number of distinct write calls to the outgoing body's output-stream + /// that the implementation will buffer. + /// Default: 1. + fn outgoing_body_buffer_chunks(&mut self) -> usize { + DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS + } + + /// Maximum size allowed in a write call to the outgoing body's output-stream. + /// Default: 1024 * 1024. + fn outgoing_body_chunk_size(&mut self) -> usize { + DEFAULT_OUTGOING_BODY_CHUNK_SIZE + } +} + +#[cfg(feature = "default-send-request")] +impl<'a> Default for &'a mut dyn WasiHttpHooks { + fn default() -> Self { + let x: &mut [(); 0] = &mut []; + x + } +} + +#[doc(hidden)] +#[cfg(feature = "default-send-request")] +impl WasiHttpHooks for [(); 0] {} + +/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has +/// the default behavior for `wasi:http`. +#[cfg(feature = "default-send-request")] +pub fn default_hooks() -> &'static mut dyn WasiHttpHooks { + Default::default() +} + +/// The default value configured for [`WasiHttpView::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. +pub const DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS: usize = 1; +/// The default value configured for [`WasiHttpView::outgoing_body_chunk_size`] in [`WasiHttpView`]. +pub const DEFAULT_OUTGOING_BODY_CHUNK_SIZE: usize = 1024 * 1024; + +/// Structure which `wasi:http` `Host`-style traits are implemented for. +/// +/// This structure is used by embedders with the [`WasiHttpView`] trait's return +/// value and is used to provide access to this crate all internals necessary to +/// implement `wasi:http`. This is similar to [`wasmtime_wasi::WasiCtxView`] +/// for example. +pub struct WasiHttpCtxView<'a> { + /// A reference to a per-store [`WasiHttpCtx`]. + pub ctx: &'a mut WasiHttpCtx, + /// A reference to a per-store table of resources to store host structures + /// within. + pub table: &'a mut ResourceTable, + /// A reference to a per-store set of hooks that can be used to customize + /// `wasi:http` behavior. + pub hooks: &'a mut dyn WasiHttpHooks, +} + +struct WasiHttp; + +impl HasData for WasiHttp { + type Data<'a> = WasiHttpCtxView<'a>; +} + +/// A trait used to project state that this crate needs to implement `wasi:http` +/// from the `self` type. +/// +/// This trait is used in [`add_to_linker_sync`] and [`add_to_linker_async`] for +/// example as a bound on `T` in `Store`. This is used to access data from +/// `T`, the data within a `Store`, an instance of [`WasiHttpCtxView`]. The +/// [`WasiHttpCtxView`] contains contextual information such as the +/// [`ResourceTable`] for the store, HTTP context info in [`WasiHttpCtx`], and +/// any hooks via [`WasiHttpHooks`] if the embedder desires. +/// +/// # Example +/// +/// ``` +/// use wasmtime::component::ResourceTable; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// struct MyState { +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// ``` +pub trait WasiHttpView { + /// Returns an instance of [`WasiHttpCtxView`] projected out of `self`. + fn http(&mut self) -> WasiHttpCtxView<'_>; +} /// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. /// @@ -244,7 +412,7 @@ use wasmtime::component::{HasData, Linker}; /// use wasmtime::{Engine, Result}; /// use wasmtime::component::{ResourceTable, Linker}; /// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p2::{WasiHttpView, WasiHttpCtxView}}; /// /// fn main() -> Result<()> { /// let engine = Engine::default(); @@ -263,8 +431,13 @@ use wasmtime::component::{HasData, Linker}; /// } /// /// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } /// } /// /// impl WasiView for MyState { @@ -293,20 +466,12 @@ where T: WasiHttpView + 'static, { let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| WasiHttpImpl(x))?; - bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; + bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, T::http)?; + bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), T::http)?; Ok(()) } -struct WasiHttp(T); - -impl HasData for WasiHttp { - type Data<'a> = WasiHttpImpl<&'a mut T>; -} - /// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. /// /// This function will add the `sync` variant of all interfaces into the @@ -319,7 +484,8 @@ impl HasData for WasiHttp { /// use wasmtime::{Engine, Result, Config}; /// use wasmtime::component::{ResourceTable, Linker}; /// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; /// /// fn main() -> Result<()> { /// let config = Config::default(); @@ -338,8 +504,13 @@ impl HasData for WasiHttp { /// table: ResourceTable, /// } /// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } /// } /// impl WasiView for MyState { /// fn ctx(&mut self) -> WasiCtxView<'_> { @@ -365,12 +536,164 @@ where T: WasiHttpView + 'static, { let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { - WasiHttpImpl(x) - })?; - bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; + bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, T::http)?; + bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), T::http)?; Ok(()) } + +/// The default implementation of how an outgoing request is sent. +/// +/// This implementation is used by the `wasi:http/outgoing-handler` interface +/// default implementation. +#[cfg(feature = "default-send-request")] +pub fn default_send_request( + request: hyper::Request, + config: types::OutgoingRequestConfig, +) -> types::HostFutureIncomingResponse { + let handle = wasmtime_wasi::runtime::spawn(async move { + Ok(default_send_request_handler(request, config).await) + }); + types::HostFutureIncomingResponse::pending(handle) +} + +/// The underlying implementation of how an outgoing request is sent. This should likely be spawned +/// in a task. +/// +/// This is called from [default_send_request] to actually send the request. +#[cfg(feature = "default-send-request")] +pub async fn default_send_request_handler( + mut request: hyper::Request, + types::OutgoingRequestConfig { + use_tls, + connect_timeout, + first_byte_timeout, + between_bytes_timeout, + }: types::OutgoingRequestConfig, +) -> Result { + use crate::io::TokioIo; + use crate::p2::{error::dns_error, hyper_request_error}; + use http_body_util::BodyExt; + use tokio::net::TcpStream; + use tokio::time::timeout; + + let authority = if let Some(authority) = request.uri().authority() { + if authority.port().is_some() { + authority.to_string() + } else { + let port = if use_tls { 443 } else { 80 }; + format!("{}:{port}", authority.to_string()) + } + } else { + return Err(ErrorCode::HttpRequestUriInvalid); + }; + let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority)) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(|e| match e.kind() { + std::io::ErrorKind::AddrNotAvailable => { + dns_error("address not available".to_string(), 0) + } + + _ => { + if e.to_string() + .starts_with("failed to lookup address information") + { + dns_error("address not available".to_string(), 0) + } else { + ErrorCode::ConnectionRefused + } + } + })?; + + let (mut sender, worker) = if use_tls { + use rustls::pki_types::ServerName; + + // derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs + let root_cert_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.into(), + }; + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_cert_store) + .with_no_client_auth(); + let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)); + let mut parts = authority.split(":"); + let host = parts.next().unwrap_or(&authority); + let domain = ServerName::try_from(host) + .map_err(|e| { + tracing::warn!("dns lookup error: {e:?}"); + dns_error("invalid dns name".to_string(), 0) + })? + .to_owned(); + let stream = connector.connect(domain, tcp_stream).await.map_err(|e| { + tracing::warn!("tls protocol error: {e:?}"); + ErrorCode::TlsProtocolError + })?; + let stream = TokioIo::new(stream); + + let (sender, conn) = timeout( + connect_timeout, + hyper::client::conn::http1::handshake(stream), + ) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(hyper_request_error)?; + + let worker = wasmtime_wasi::runtime::spawn(async move { + match conn.await { + Ok(()) => {} + // TODO: shouldn't throw away this error and ideally should + // surface somewhere. + Err(e) => tracing::warn!("dropping error {e}"), + } + }); + + (sender, worker) + } else { + let tcp_stream = TokioIo::new(tcp_stream); + let (sender, conn) = timeout( + connect_timeout, + // TODO: we should plumb the builder through the http context, and use it here + hyper::client::conn::http1::handshake(tcp_stream), + ) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(hyper_request_error)?; + + let worker = wasmtime_wasi::runtime::spawn(async move { + match conn.await { + Ok(()) => {} + // TODO: same as above, shouldn't throw this error away. + Err(e) => tracing::warn!("dropping error {e}"), + } + }); + + (sender, worker) + }; + + // at this point, the request contains the scheme and the authority, but + // the http packet should only include those if addressing a proxy, so + // remove them here, since SendRequest::send_request does not do it for us + *request.uri_mut() = http::Uri::builder() + .path_and_query( + request + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or("/"), + ) + .build() + .expect("comes from valid request"); + + let resp = timeout(first_byte_timeout, sender.send_request(request)) + .await + .map_err(|_| ErrorCode::ConnectionReadTimeout)? + .map_err(hyper_request_error)? + .map(|body| body.map_err(hyper_request_error).boxed_unsync()); + + Ok(types::IncomingResponse { + resp, + worker: Some(worker), + between_bytes_timeout, + }) +} diff --git a/crates/wasi-http/src/p2/types.rs b/crates/wasi-http/src/p2/types.rs index 88fcf691872e..fcdf2a45bdaa 100644 --- a/crates/wasi-http/src/p2/types.rs +++ b/crates/wasi-http/src/p2/types.rs @@ -2,11 +2,10 @@ //! implementation of the wasi-http API. use crate::p2::{ - HttpResult, + WasiHttpCtxView, WasiHttpHooks, bindings::http::types::{self, ErrorCode, Method, Scheme}, body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody}, }; -use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; use bytes::Bytes; use http::header::{HeaderMap, HeaderName, HeaderValue}; use http_body_util::BodyExt; @@ -14,281 +13,15 @@ use hyper::body::Body; use std::any::Any; use std::fmt; use std::time::Duration; -use wasmtime::component::{Resource, ResourceTable}; +use wasmtime::component::Resource; use wasmtime::{Result, bail}; use wasmtime_wasi::p2::Pollable; use wasmtime_wasi::runtime::AbortOnDropJoinHandle; -#[cfg(feature = "default-send-request")] -use { - crate::io::TokioIo, - crate::p2::{error::dns_error, hyper_request_error}, - tokio::net::TcpStream, - tokio::time::timeout, -}; - -/// A trait which provides internal WASI HTTP state. -/// -/// # Example -/// -/// ``` -/// use wasmtime::component::ResourceTable; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// -/// impl MyState { -/// fn new() -> MyState { -/// let mut wasi = WasiCtx::builder(); -/// wasi.arg("./foo.wasm"); -/// wasi.arg("--help"); -/// wasi.env("FOO", "bar"); -/// -/// MyState { -/// ctx: wasi.build(), -/// table: ResourceTable::new(), -/// http_ctx: WasiHttpCtx::new(), -/// } -/// } -/// } -/// ``` -pub trait WasiHttpView { - /// Returns a mutable reference to the WASI HTTP context. - fn ctx(&mut self) -> &mut WasiHttpCtx; - - /// Returns the table used to manage resources. - fn table(&mut self) -> &mut ResourceTable; - - /// Create a new incoming request resource. - fn new_incoming_request( - &mut self, - scheme: Scheme, - req: hyper::Request, - ) -> wasmtime::Result> - where - B: Body + Send + 'static, - B::Error: Into, - Self: Sized, - { - let field_size_limit = self.ctx().field_size_limit; - let (parts, body) = req.into_parts(); - let body = body.map_err(Into::into).boxed_unsync(); - let body = HostIncomingBody::new( - body, - // TODO: this needs to be plumbed through - std::time::Duration::from_millis(600 * 1000), - field_size_limit, - ); - let incoming_req = - HostIncomingRequest::new(self, parts, scheme, Some(body), field_size_limit)?; - Ok(self.table().push(incoming_req)?) - } - - /// Create a new outgoing response resource. - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - let id = self.table().push(HostResponseOutparam { result })?; - Ok(id) - } - - /// Send an outgoing request. - #[cfg(feature = "default-send-request")] - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> HttpResult { - Ok(default_send_request(request, config)) - } - - /// Send an outgoing request. - #[cfg(not(feature = "default-send-request"))] - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> HttpResult; - - /// Whether a given header should be considered forbidden and not allowed. - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - DEFAULT_FORBIDDEN_HEADERS.contains(name) - } - - /// Number of distinct write calls to the outgoing body's output-stream - /// that the implementation will buffer. - /// Default: 1. - fn outgoing_body_buffer_chunks(&mut self) -> usize { - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS - } - - /// Maximum size allowed in a write call to the outgoing body's output-stream. - /// Default: 1024 * 1024. - fn outgoing_body_chunk_size(&mut self) -> usize { - DEFAULT_OUTGOING_BODY_CHUNK_SIZE - } -} - -/// The default value configured for [`WasiHttpView::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. -pub const DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS: usize = 1; -/// The default value configured for [`WasiHttpView::outgoing_body_chunk_size`] in [`WasiHttpView`]. -pub const DEFAULT_OUTGOING_BODY_CHUNK_SIZE: usize = 1024 * 1024; - -impl WasiHttpView for &mut T { - fn ctx(&mut self) -> &mut WasiHttpCtx { - T::ctx(self) - } - - fn table(&mut self) -> &mut ResourceTable { - T::table(self) - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - T::new_response_outparam(self, result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> HttpResult { - T::send_request(self, request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - T::is_forbidden_header(self, name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - T::outgoing_body_buffer_chunks(self) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - T::outgoing_body_chunk_size(self) - } -} - -impl WasiHttpView for Box { - fn ctx(&mut self) -> &mut WasiHttpCtx { - T::ctx(self) - } - - fn table(&mut self) -> &mut ResourceTable { - T::table(self) - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - T::new_response_outparam(self, result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> HttpResult { - T::send_request(self, request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - T::is_forbidden_header(self, name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - T::outgoing_body_buffer_chunks(self) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - T::outgoing_body_chunk_size(self) - } -} - -/// A concrete structure that all generated `Host` traits are implemented for. -/// -/// This type serves as a small newtype wrapper to implement all of the `Host` -/// traits for `wasi:http`. This type is internally used and is only needed if -/// you're interacting with `add_to_linker` functions generated by bindings -/// themselves (or `add_to_linker_get_host`). -/// -/// This type is automatically used when using -/// [`add_to_linker_async`](crate::add_to_linker_async) -/// or -/// [`add_to_linker_sync`](crate::add_to_linker_sync) -/// and doesn't need to be manually configured. -#[repr(transparent)] -pub struct WasiHttpImpl(pub T); - -impl WasiHttpView for WasiHttpImpl { - fn ctx(&mut self) -> &mut WasiHttpCtx { - self.0.ctx() - } - - fn table(&mut self) -> &mut ResourceTable { - self.0.table() - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - self.0.new_response_outparam(result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> HttpResult { - self.0.send_request(request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - self.0.is_forbidden_header(name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.0.outgoing_body_buffer_chunks() - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.0.outgoing_body_chunk_size() - } -} - /// Removes forbidden headers from a [`FieldMap`]. -pub(crate) fn remove_forbidden_headers(view: &mut dyn WasiHttpView, headers: &mut FieldMap) { +pub(crate) fn remove_forbidden_headers(hooks: &mut dyn WasiHttpHooks, headers: &mut FieldMap) { let forbidden_keys = Vec::from_iter(headers.as_ref().keys().filter_map(|name| { - if view.is_forbidden_header(name) { + if hooks.is_forbidden_header(name) { Some(name.clone()) } else { None @@ -312,156 +45,6 @@ pub struct OutgoingRequestConfig { pub between_bytes_timeout: Duration, } -/// The default implementation of how an outgoing request is sent. -/// -/// This implementation is used by the `wasi:http/outgoing-handler` interface -/// default implementation. -#[cfg(feature = "default-send-request")] -pub fn default_send_request( - request: hyper::Request, - config: OutgoingRequestConfig, -) -> HostFutureIncomingResponse { - let handle = wasmtime_wasi::runtime::spawn(async move { - Ok(default_send_request_handler(request, config).await) - }); - HostFutureIncomingResponse::pending(handle) -} - -/// The underlying implementation of how an outgoing request is sent. This should likely be spawned -/// in a task. -/// -/// This is called from [default_send_request] to actually send the request. -#[cfg(feature = "default-send-request")] -pub async fn default_send_request_handler( - mut request: hyper::Request, - OutgoingRequestConfig { - use_tls, - connect_timeout, - first_byte_timeout, - between_bytes_timeout, - }: OutgoingRequestConfig, -) -> Result { - let authority = if let Some(authority) = request.uri().authority() { - if authority.port().is_some() { - authority.to_string() - } else { - let port = if use_tls { 443 } else { 80 }; - format!("{}:{port}", authority.to_string()) - } - } else { - return Err(types::ErrorCode::HttpRequestUriInvalid); - }; - let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority)) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(|e| match e.kind() { - std::io::ErrorKind::AddrNotAvailable => { - dns_error("address not available".to_string(), 0) - } - - _ => { - if e.to_string() - .starts_with("failed to lookup address information") - { - dns_error("address not available".to_string(), 0) - } else { - types::ErrorCode::ConnectionRefused - } - } - })?; - - let (mut sender, worker) = if use_tls { - use rustls::pki_types::ServerName; - - // derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs - let root_cert_store = rustls::RootCertStore { - roots: webpki_roots::TLS_SERVER_ROOTS.into(), - }; - let config = rustls::ClientConfig::builder() - .with_root_certificates(root_cert_store) - .with_no_client_auth(); - let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)); - let mut parts = authority.split(":"); - let host = parts.next().unwrap_or(&authority); - let domain = ServerName::try_from(host) - .map_err(|e| { - tracing::warn!("dns lookup error: {e:?}"); - dns_error("invalid dns name".to_string(), 0) - })? - .to_owned(); - let stream = connector.connect(domain, tcp_stream).await.map_err(|e| { - tracing::warn!("tls protocol error: {e:?}"); - types::ErrorCode::TlsProtocolError - })?; - let stream = TokioIo::new(stream); - - let (sender, conn) = timeout( - connect_timeout, - hyper::client::conn::http1::handshake(stream), - ) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(hyper_request_error)?; - - let worker = wasmtime_wasi::runtime::spawn(async move { - match conn.await { - Ok(()) => {} - // TODO: shouldn't throw away this error and ideally should - // surface somewhere. - Err(e) => tracing::warn!("dropping error {e}"), - } - }); - - (sender, worker) - } else { - let tcp_stream = TokioIo::new(tcp_stream); - let (sender, conn) = timeout( - connect_timeout, - // TODO: we should plumb the builder through the http context, and use it here - hyper::client::conn::http1::handshake(tcp_stream), - ) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(hyper_request_error)?; - - let worker = wasmtime_wasi::runtime::spawn(async move { - match conn.await { - Ok(()) => {} - // TODO: same as above, shouldn't throw this error away. - Err(e) => tracing::warn!("dropping error {e}"), - } - }); - - (sender, worker) - }; - - // at this point, the request contains the scheme and the authority, but - // the http packet should only include those if addressing a proxy, so - // remove them here, since SendRequest::send_request does not do it for us - *request.uri_mut() = http::Uri::builder() - .path_and_query( - request - .uri() - .path_and_query() - .map(|p| p.as_str()) - .unwrap_or("/"), - ) - .build() - .expect("comes from valid request"); - - let resp = timeout(first_byte_timeout, sender.send_request(request)) - .await - .map_err(|_| types::ErrorCode::ConnectionReadTimeout)? - .map_err(hyper_request_error)? - .map(|body| body.map_err(hyper_request_error).boxed_unsync()); - - Ok(IncomingResponse { - resp, - worker: Some(worker), - between_bytes_timeout, - }) -} - impl From for types::Method { fn from(method: http::Method) -> Self { if method == http::Method::GET { @@ -519,15 +102,26 @@ pub struct HostIncomingRequest { pub body: Option, } -impl HostIncomingRequest { - /// Create a new `HostIncomingRequest`. - pub fn new( - view: &mut dyn WasiHttpView, - parts: http::request::Parts, +impl WasiHttpCtxView<'_> { + /// Create a new incoming request resource. + pub fn new_incoming_request( + &mut self, scheme: Scheme, - body: Option, - field_size_limit: usize, - ) -> wasmtime::Result { + req: hyper::Request, + ) -> wasmtime::Result> + where + B: Body + Send + 'static, + B::Error: Into, + { + let field_size_limit = self.ctx.field_size_limit; + let (parts, body) = req.into_parts(); + let body = body.map_err(Into::into).boxed_unsync(); + let body = HostIncomingBody::new( + body, + // TODO: this needs to be plumbed through + std::time::Duration::from_millis(600 * 1000), + field_size_limit, + ); let authority = match parts.uri.authority() { Some(authority) => authority.to_string(), None => match parts.headers.get(http::header::HOST) { @@ -537,16 +131,17 @@ impl HostIncomingRequest { }; let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(view, &mut headers); + remove_forbidden_headers(self.hooks, &mut headers); - Ok(Self { + let req = HostIncomingRequest { method: parts.method, uri: parts.uri, headers, authority, scheme, - body, - }) + body: Some(body), + }; + Ok(self.table.push(req)?) } } @@ -557,6 +152,19 @@ pub struct HostResponseOutparam { tokio::sync::oneshot::Sender, types::ErrorCode>>, } +impl WasiHttpCtxView<'_> { + /// Create a new outgoing response resource. + pub fn new_response_outparam( + &mut self, + result: tokio::sync::oneshot::Sender< + Result, types::ErrorCode>, + >, + ) -> wasmtime::Result> { + let id = self.table.push(HostResponseOutparam { result })?; + Ok(id) + } +} + /// The concrete type behind a `wasi:http/types.outgoing-response` resource. pub struct HostOutgoingResponse { /// The status of the response. diff --git a/crates/wasi-http/src/p2/types_impl.rs b/crates/wasi-http/src/p2/types_impl.rs index d0b468e1f53a..3bf7e377cc07 100644 --- a/crates/wasi-http/src/p2/types_impl.rs +++ b/crates/wasi-http/src/p2/types_impl.rs @@ -8,7 +8,7 @@ use crate::p2::types::{ HostIncomingResponse, HostOutgoingRequest, HostOutgoingResponse, HostResponseOutparam, remove_forbidden_headers, }; -use crate::p2::{HttpError, HttpResult, WasiHttpImpl, WasiHttpView}; +use crate::p2::{HttpError, HttpResult, WasiHttpCtxView}; use std::any::Any; use std::str::FromStr; use wasmtime::bail; @@ -16,10 +16,7 @@ use wasmtime::component::{Resource, ResourceTable, ResourceTableError}; use wasmtime::{error::Context as _, format_err}; use wasmtime_wasi::p2::{DynInputStream, DynOutputStream, DynPollable}; -impl types::Host for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::Host for WasiHttpCtxView<'_> { fn convert_error_code(&mut self, err: HttpError) -> wasmtime::Result { err.downcast() } @@ -28,7 +25,7 @@ where &mut self, err: wasmtime::component::Resource, ) -> wasmtime::Result> { - let e = self.table().get(&err)?; + let e = self.table.get(&err)?; Ok(e.downcast_ref::().cloned()) } } @@ -78,14 +75,11 @@ fn get_fields_mut<'a>( } } -impl types::HostFields for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFields for WasiHttpCtxView<'_> { fn new(&mut self) -> wasmtime::Result> { - let limit = self.ctx().field_size_limit; + let limit = self.ctx.field_size_limit; let id = self - .table() + .table .push(HostFields::Owned { fields: FieldMap::empty(limit), }) @@ -106,7 +100,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -119,15 +113,15 @@ where } let size = FieldMap::content_size(&fields); - if size > self.ctx().field_size_limit { + if size > self.ctx.field_size_limit { bail!(FieldSizeLimitError { size, - limit: self.ctx().field_size_limit, + limit: self.ctx.field_size_limit, }); } - let fields = FieldMap::new(fields, self.ctx().field_size_limit); + let fields = FieldMap::new(fields, self.ctx.field_size_limit); let id = self - .table() + .table .push(HostFields::Owned { fields }) .context("[new_fields] pushing fields")?; @@ -135,7 +129,7 @@ where } fn drop(&mut self, fields: Resource) -> wasmtime::Result<()> { - self.table() + self.table .delete(fields) .context("[drop_fields] deleting fields")?; Ok(()) @@ -146,7 +140,7 @@ where fields: Resource, name: String, ) -> wasmtime::Result>> { - let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?; + let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) { Ok(header) => header, @@ -167,7 +161,7 @@ where } fn has(&mut self, fields: Resource, name: String) -> wasmtime::Result { - let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?; + let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; match hyper::header::HeaderName::from_bytes(name.as_bytes()) { Ok(header) => Ok(fields.as_ref().contains_key(&header)), @@ -186,7 +180,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -198,9 +192,7 @@ where } } - match get_fields_mut(self.table(), &fields) - .context("[fields_set] getting mutable fields")? - { + match get_fields_mut(self.table, &fields).context("[fields_set] getting mutable fields")? { Ok(fields) => { fields.remove_all(&header); for value in values { @@ -222,11 +214,11 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } - Ok(get_fields_mut(self.table(), &fields)?.map(|fields| { + Ok(get_fields_mut(self.table, &fields)?.map(|fields| { fields.remove_all(&header); })) } @@ -242,7 +234,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -251,7 +243,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - match get_fields_mut(self.table(), &fields) + match get_fields_mut(self.table, &fields) .context("[fields_append] getting mutable fields")? { Ok(fields) => { @@ -266,7 +258,7 @@ where &mut self, fields: Resource, ) -> wasmtime::Result)>> { - Ok(get_fields(self.table(), &fields)? + Ok(get_fields(self.table, &fields)? .as_ref() .iter() .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_owned())) @@ -274,12 +266,12 @@ where } fn clone(&mut self, fields: Resource) -> wasmtime::Result> { - let fields = get_fields(self.table(), &fields) + let fields = get_fields(self.table, &fields) .context("[fields_clone] getting fields")? .clone(); let id = self - .table() + .table .push(HostFields::Owned { fields }) .context("[fields_clone] pushing fields")?; @@ -287,30 +279,27 @@ where } } -impl types::HostIncomingRequest for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingRequest for WasiHttpCtxView<'_> { fn method(&mut self, id: Resource) -> wasmtime::Result { - let method = self.table().get(&id)?.method.clone(); + let method = self.table.get(&id)?.method.clone(); Ok(method.into()) } fn path_with_query( &mut self, id: Resource, ) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(req .uri .path_and_query() .map(|path_and_query| path_and_query.as_str().to_owned())) } fn scheme(&mut self, id: Resource) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(Some(req.scheme.clone())) } fn authority(&mut self, id: Resource) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(Some(req.authority.clone())) } @@ -318,13 +307,13 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - let _ = self.table().get(&id)?; + let _ = self.table.get(&id)?; fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { &mut elem.downcast_mut::().unwrap().headers } - let headers = self.table().push_child( + let headers = self.table.push_child( HostFields::Ref { parent: id.rep(), get_fields, @@ -339,10 +328,10 @@ where &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let req = self.table().get_mut(&id)?; + let req = self.table.get_mut(&id)?; match req.body.take() { Some(body) => { - let id = self.table().push(body)?; + let id = self.table.push(body)?; Ok(Ok(id)) } @@ -351,22 +340,19 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl types::HostOutgoingRequest for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingRequest for WasiHttpCtxView<'_> { fn new( &mut self, headers: Resource, ) -> wasmtime::Result> { - let headers = move_fields(self.table(), headers)?; + let headers = move_fields(self.table, headers)?; - self.table() + self.table .push(HostOutgoingRequest { path_with_query: None, authority: None, @@ -382,10 +368,10 @@ where &mut self, request: Resource, ) -> wasmtime::Result, ()>> { - let buffer_chunks = self.outgoing_body_buffer_chunks(); - let chunk_size = self.outgoing_body_chunk_size(); + let buffer_chunks = self.hooks.outgoing_body_buffer_chunks(); + let chunk_size = self.hooks.outgoing_body_chunk_size(); let req = self - .table() + .table .get_mut(&request) .context("[outgoing_request_write] getting request")?; @@ -405,13 +391,13 @@ where // The output stream will necessarily outlive the request, because we could be still // writing to the stream after `outgoing-handler.handle` is called. - let outgoing_body = self.table().push(host_body)?; + let outgoing_body = self.table.push(host_body)?; Ok(Ok(outgoing_body)) } fn drop(&mut self, request: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(request)?; + let _ = self.table.delete(request)?; Ok(()) } @@ -419,7 +405,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result { - Ok(self.table().get(&request)?.method.clone()) + Ok(self.table.get(&request)?.method.clone()) } fn set_method( @@ -427,7 +413,7 @@ where request: wasmtime::component::Resource, method: Method, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Method::Other(s) = &method { if let Err(_) = http::Method::from_str(s) { @@ -444,7 +430,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.path_with_query.clone()) + Ok(self.table.get(&request)?.path_with_query.clone()) } fn set_path_with_query( @@ -452,7 +438,7 @@ where request: wasmtime::component::Resource, path_with_query: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(s) = path_with_query.as_ref() { if let Err(_) = http::uri::PathAndQuery::from_str(s) { @@ -469,7 +455,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.scheme.clone()) + Ok(self.table.get(&request)?.scheme.clone()) } fn set_scheme( @@ -477,7 +463,7 @@ where request: wasmtime::component::Resource, scheme: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(types::Scheme::Other(s)) = scheme.as_ref() { if let Err(_) = http::uri::Scheme::from_str(s.as_str()) { @@ -494,7 +480,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.authority.clone()) + Ok(self.table.get(&request)?.authority.clone()) } fn set_authority( @@ -502,7 +488,7 @@ where request: wasmtime::component::Resource, authority: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(s) = authority.as_ref() { if let Err(_) = http::uri::Authority::from_str(s.as_str()) { @@ -520,7 +506,7 @@ where request: wasmtime::component::Resource, ) -> wasmtime::Result> { let _ = self - .table() + .table .get(&request) .context("[outgoing_request_headers] getting request")?; @@ -531,7 +517,7 @@ where .headers } - let id = self.table().push_child( + let id = self.table.push_child( HostFields::Ref { parent: request.rep(), get_fields, @@ -543,12 +529,9 @@ where } } -impl types::HostResponseOutparam for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostResponseOutparam for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } fn set( @@ -557,11 +540,11 @@ where resp: Result, types::ErrorCode>, ) -> wasmtime::Result<()> { let val = match resp { - Ok(resp) => Ok(self.table().delete(resp)?.try_into()?), + Ok(resp) => Ok(self.table.delete(resp)?.try_into()?), Err(e) => Err(e), }; - let resp = self.table().delete(id)?; + let resp = self.table.delete(id)?; // Giving the API doesn't return any error, it's probably // better to ignore the error than trap the guest, in case of // host timeout and dropped the receiver side of the channel. @@ -580,13 +563,10 @@ where } } -impl types::HostIncomingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingResponse for WasiHttpCtxView<'_> { fn drop(&mut self, response: Resource) -> wasmtime::Result<()> { let _ = self - .table() + .table .delete(response) .context("[drop_incoming_response] deleting response")?; Ok(()) @@ -594,7 +574,7 @@ where fn status(&mut self, response: Resource) -> wasmtime::Result { let r = self - .table() + .table .get(&response) .context("[incoming_response_status] getting response")?; Ok(r.status) @@ -605,7 +585,7 @@ where response: Resource, ) -> wasmtime::Result> { let _ = self - .table() + .table .get(&response) .context("[incoming_response_headers] getting response")?; @@ -613,7 +593,7 @@ where &mut elem.downcast_mut::().unwrap().headers } - let id = self.table().push_child( + let id = self.table.push_child( HostFields::Ref { parent: response.rep(), get_fields, @@ -628,14 +608,14 @@ where &mut self, response: Resource, ) -> wasmtime::Result, ()>> { - let table = self.table(); - let r = table + let r = self + .table .get_mut(&response) .context("[incoming_response_consume] getting response")?; match r.body.take() { Some(body) => { - let id = self.table().push(body)?; + let id = self.table.push(body)?; Ok(Ok(id)) } @@ -644,13 +624,10 @@ where } } -impl types::HostFutureTrailers for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFutureTrailers for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { let _ = self - .table() + .table .delete(id) .context("[drop future-trailers] deleting future-trailers")?; Ok(()) @@ -660,7 +637,7 @@ where &mut self, index: Resource, ) -> wasmtime::Result> { - wasmtime_wasi::p2::subscribe(self.table(), index) + wasmtime_wasi::p2::subscribe(self.table, index) } fn get( @@ -668,7 +645,7 @@ where id: Resource, ) -> wasmtime::Result>, types::ErrorCode>, ()>>> { - let trailers = self.table().get_mut(&id)?; + let trailers = self.table.get_mut(&id)?; match trailers { HostFutureTrailers::Waiting { .. } => return Ok(None), HostFutureTrailers::Consumed => return Ok(Some(Err(()))), @@ -686,27 +663,24 @@ where Err(e) => return Ok(Some(Ok(Err(e)))), }; - remove_forbidden_headers(self, &mut fields); + remove_forbidden_headers(self.hooks, &mut fields); - let ts = self.table().push(HostFields::Owned { fields })?; + let ts = self.table.push(HostFields::Owned { fields })?; Ok(Some(Ok(Ok(Some(ts))))) } } -impl types::HostIncomingBody for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingBody for WasiHttpCtxView<'_> { fn stream( &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let body = self.table().get_mut(&id)?; + let body = self.table.get_mut(&id)?; if let Some(stream) = body.take_stream() { let stream: DynInputStream = Box::new(stream); - let stream = self.table().push_child(stream, &id)?; + let stream = self.table.push_child(stream, &id)?; return Ok(Ok(stream)); } @@ -717,28 +691,25 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - let body = self.table().delete(id)?; - let trailers = self.table().push(body.into_future_trailers())?; + let body = self.table.delete(id)?; + let trailers = self.table.push(body.into_future_trailers())?; Ok(trailers) } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl types::HostOutgoingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingResponse for WasiHttpCtxView<'_> { fn new( &mut self, headers: Resource, ) -> wasmtime::Result> { - let fields = move_fields(self.table(), headers)?; + let fields = move_fields(self.table, headers)?; - let id = self.table().push(HostOutgoingResponse { + let id = self.table.push(HostOutgoingResponse { status: http::StatusCode::OK, headers: fields, body: None, @@ -751,9 +722,9 @@ where &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let buffer_chunks = self.outgoing_body_buffer_chunks(); - let chunk_size = self.outgoing_body_chunk_size(); - let resp = self.table().get_mut(&id)?; + let buffer_chunks = self.hooks.outgoing_body_buffer_chunks(); + let chunk_size = self.hooks.outgoing_body_chunk_size(); + let resp = self.table.get_mut(&id)?; if resp.body.is_some() { return Ok(Err(())); @@ -769,7 +740,7 @@ where resp.body.replace(body); - let id = self.table().push(host)?; + let id = self.table.push(host)?; Ok(Ok(id)) } @@ -778,7 +749,7 @@ where &mut self, id: Resource, ) -> wasmtime::Result { - Ok(self.table().get(&id)?.status.into()) + Ok(self.table.get(&id)?.status.into()) } fn set_status_code( @@ -786,7 +757,7 @@ where id: Resource, status: types::StatusCode, ) -> wasmtime::Result> { - let resp = self.table().get_mut(&id)?; + let resp = self.table.get_mut(&id)?; match http::StatusCode::from_u16(status) { Ok(status) => resp.status = status, @@ -801,14 +772,14 @@ where id: Resource, ) -> wasmtime::Result> { // Trap if the outgoing-response doesn't exist. - let _ = self.table().get(&id)?; + let _ = self.table.get(&id)?; fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { let resp = elem.downcast_mut::().unwrap(); &mut resp.headers } - Ok(self.table().push_child( + Ok(self.table.push_child( HostFields::Ref { parent: id.rep(), get_fields, @@ -818,17 +789,14 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl types::HostFutureIncomingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFutureIncomingResponse for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } @@ -838,8 +806,8 @@ where ) -> wasmtime::Result< Option, types::ErrorCode>, ()>>, > { - let field_size_limit = self.ctx().field_size_limit; - let resp = self.table().get_mut(&id)?; + let field_size_limit = self.ctx.field_size_limit; + let resp = self.table.get_mut(&id)?; match resp { HostFutureIncomingResponse::Pending(_) => return Ok(None), @@ -862,9 +830,9 @@ where let (parts, body) = resp.resp.into_parts(); let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(self, &mut headers); + remove_forbidden_headers(self.hooks, &mut headers); - let resp = self.table().push(HostIncomingResponse { + let resp = self.table.push(HostIncomingResponse { status: parts.status.as_u16(), headers, body: Some({ @@ -884,21 +852,18 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - wasmtime_wasi::p2::subscribe(self.table(), id) + wasmtime_wasi::p2::subscribe(self.table, id) } } -impl types::HostOutgoingBody for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingBody for WasiHttpCtxView<'_> { fn write( &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let body = self.table().get_mut(&id)?; + let body = self.table.get_mut(&id)?; if let Some(stream) = body.take_output_stream() { - let id = self.table().push_child(stream, &id)?; + let id = self.table.push_child(stream, &id)?; Ok(Ok(id)) } else { Ok(Err(())) @@ -910,10 +875,10 @@ where id: Resource, ts: Option>, ) -> HttpResult<()> { - let body = self.table().delete(id)?; + let body = self.table.delete(id)?; let ts = if let Some(ts) = ts { - Some(move_fields(self.table(), ts)?) + Some(move_fields(self.table, ts)?) } else { None }; @@ -923,17 +888,14 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - self.table().delete(id)?.abort(); + self.table.delete(id)?.abort(); Ok(()) } } -impl types::HostRequestOptions for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostRequestOptions for WasiHttpCtxView<'_> { fn new(&mut self) -> wasmtime::Result> { - let id = self.table().push(types::RequestOptions::default())?; + let id = self.table.push(types::RequestOptions::default())?; Ok(id) } @@ -941,11 +903,7 @@ where &mut self, opts: Resource, ) -> wasmtime::Result> { - let nanos = self - .table() - .get(&opts)? - .connect_timeout - .map(|d| d.as_nanos()); + let nanos = self.table.get(&opts)?.connect_timeout.map(|d| d.as_nanos()); if let Some(nanos) = nanos { Ok(Some(nanos.try_into()?)) @@ -959,8 +917,7 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.connect_timeout = - duration.map(std::time::Duration::from_nanos); + self.table.get_mut(&opts)?.connect_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } @@ -969,7 +926,7 @@ where opts: Resource, ) -> wasmtime::Result> { let nanos = self - .table() + .table .get(&opts)? .first_byte_timeout .map(|d| d.as_nanos()); @@ -986,7 +943,7 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.first_byte_timeout = + self.table.get_mut(&opts)?.first_byte_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } @@ -996,7 +953,7 @@ where opts: Resource, ) -> wasmtime::Result> { let nanos = self - .table() + .table .get(&opts)? .between_bytes_timeout .map(|d| d.as_nanos()); @@ -1013,13 +970,13 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.between_bytes_timeout = + self.table.get_mut(&opts)?.between_bytes_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(rep)?; + let _ = self.table.delete(rep)?; Ok(()) } } diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index 8e57d898257b..ffa2fea1baa6 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -58,7 +58,7 @@ impl HostWithStore for WasiHttp { .map_err(HttpError::trap)?; let (req, options) = req.into_http_with_getter(&mut store, io_task_result(io_result_rx), getter)?; - HttpResult::Ok(store.get().ctx.send_request( + HttpResult::Ok(store.get().hooks.send_request( req.map(|body| body.with_state(io_task_rx).boxed_unsync()), options.as_deref().copied(), Box::new(async { diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index ff7f397f7665..6179bea36d26 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -189,7 +189,7 @@ impl HostFields for WasiHttpCtxView<'_> { let mut fields = http::HeaderMap::default(); for (name, value) in entries { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; @@ -225,7 +225,7 @@ impl HostFields for WasiHttpCtxView<'_> { value: Vec, ) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let mut values = Vec::with_capacity(value.len()); @@ -244,7 +244,7 @@ impl HostFields for WasiHttpCtxView<'_> { fn delete(&mut self, fields: Resource, name: FieldName) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let fields = get_fields_mut(self.table, &fields)?; @@ -259,7 +259,7 @@ impl HostFields for WasiHttpCtxView<'_> { name: FieldName, ) -> HeaderResult> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let fields = get_fields_mut(self.table, &fields)?; @@ -278,7 +278,7 @@ impl HostFields for WasiHttpCtxView<'_> { value: FieldValue, ) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index 59048fdca154..7af0829cff23 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -21,8 +21,8 @@ pub use request::default_send_request; pub use request::{Request, RequestOptions}; pub use response::Response; -use crate::DEFAULT_FORBIDDEN_HEADERS; use crate::p3::bindings::http::types::ErrorCode; +use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; use bindings::http::{client, types}; use bytes::Bytes; use core::ops::Deref; @@ -50,7 +50,7 @@ impl HasData for WasiHttp { } /// A trait which provides internal WASI HTTP state. -pub trait WasiHttpCtx: Send { +pub trait WasiHttpHooks: Send { /// Whether a given header should be considered forbidden and not allowed. fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { DEFAULT_FORBIDDEN_HEADERS.contains(name) @@ -150,21 +150,35 @@ pub trait WasiHttpCtx: Send { >; } -/// Default implementation of [WasiHttpCtx]. #[cfg(feature = "default-send-request")] -#[derive(Clone, Default)] -pub struct DefaultWasiHttpCtx; +impl<'a> Default for &'a mut dyn WasiHttpHooks { + fn default() -> Self { + let x: &mut [(); 0] = &mut []; + x + } +} + +#[doc(hidden)] +#[cfg(feature = "default-send-request")] +impl WasiHttpHooks for [(); 0] {} +/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has +/// the default behavior for `wasi:http`. #[cfg(feature = "default-send-request")] -impl WasiHttpCtx for DefaultWasiHttpCtx {} +pub fn default_hooks() -> &'static mut dyn WasiHttpHooks { + Default::default() +} /// View into [WasiHttpCtx] implementation and [ResourceTable]. pub struct WasiHttpCtxView<'a> { - /// Mutable reference to the WASI HTTP context. - pub ctx: &'a mut dyn WasiHttpCtx, + /// Mutable reference to the WASI HTTP hooks. + pub hooks: &'a mut dyn WasiHttpHooks, /// Mutable reference to table used to manage resources. pub table: &'a mut ResourceTable, + + /// Mutable reference to the WASI HTTP context. + pub ctx: &'a mut WasiHttpCtx, } /// A trait which provides internal WASI HTTP state. diff --git a/crates/wasi-http/src/p3/request.rs b/crates/wasi-http/src/p3/request.rs index e0671fc0dba2..458919f56fd2 100644 --- a/crates/wasi-http/src/p3/request.rs +++ b/crates/wasi-http/src/p3/request.rs @@ -209,8 +209,8 @@ impl Request { }; let mut headers = Arc::unwrap_or_clone(headers); let mut store = store.as_context_mut(); - let WasiHttpCtxView { ctx, .. } = getter(store.data_mut()); - if ctx.set_host_header() { + let WasiHttpCtxView { hooks, .. } = getter(store.data_mut()); + if hooks.set_host_header() { let host = if let Some(authority) = authority.as_ref() { HeaderValue::try_from(authority.as_str()) .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))? @@ -220,8 +220,8 @@ impl Request { headers.insert(HOST, host); } let scheme = match scheme { - None => ctx.default_scheme().ok_or(ErrorCode::HttpProtocolError)?, - Some(scheme) if ctx.is_supported_scheme(&scheme) => scheme, + None => hooks.default_scheme().ok_or(ErrorCode::HttpProtocolError)?, + Some(scheme) if hooks.is_supported_scheme(&scheme) => scheme, Some(..) => return Err(ErrorCode::HttpProtocolError.into()), }; let mut uri = Uri::builder().scheme(scheme); @@ -478,7 +478,7 @@ pub async fn default_send_request( #[cfg(test)] mod tests { use super::*; - use crate::p3::DefaultWasiHttpCtx; + use crate::WasiHttpCtx; use core::future::Future; use core::pin::pin; use core::str::FromStr; @@ -491,7 +491,7 @@ mod tests { struct TestCtx { table: ResourceTable, wasi: WasiCtx, - http: DefaultWasiHttpCtx, + http: WasiHttpCtx, } impl TestCtx { @@ -499,7 +499,7 @@ mod tests { Self { table: ResourceTable::default(), wasi: WasiCtxBuilder::new().build(), - http: DefaultWasiHttpCtx, + http: Default::default(), } } } @@ -518,6 +518,7 @@ mod tests { WasiHttpCtxView { ctx: &mut self.http, table: &mut self.table, + hooks: crate::p3::default_hooks(), } } } diff --git a/crates/wasi-http/tests/all/p2.rs b/crates/wasi-http/tests/all/p2.rs index cee6dd3e860d..7de12249ed4c 100644 --- a/crates/wasi-http/tests/all/p2.rs +++ b/crates/wasi-http/tests/all/p2.rs @@ -20,7 +20,7 @@ use wasmtime_wasi_http::{ p2::bindings::http::types::{ErrorCode, Scheme}, p2::body::HyperOutgoingBody, p2::types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, - p2::{HttpResult, WasiHttpView}, + p2::{HttpResult, WasiHttpCtxView, WasiHttpHooks, WasiHttpView}, }; type RequestSender = Arc< @@ -35,6 +35,10 @@ struct Ctx { http: WasiHttpCtx, stdout: MemoryOutputPipe, stderr: MemoryOutputPipe, + hooks: MyHttpHooks, +} + +struct MyHttpHooks { send_request: Option, rejected_authority: Option, } @@ -49,14 +53,16 @@ impl WasiView for Ctx { } impl WasiHttpView for Ctx { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http - } - - fn table(&mut self) -> &mut ResourceTable { - &mut self.table + fn http(&mut self) -> WasiHttpCtxView<'_> { + WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.hooks, + } } +} +impl WasiHttpHooks for MyHttpHooks { fn send_request( &mut self, request: hyper::Request, @@ -71,7 +77,9 @@ impl WasiHttpView for Ctx { if let Some(send_request) = self.send_request.clone() { Ok(send_request(request, config)) } else { - Ok(types::default_send_request(request, config)) + Ok(wasmtime_wasi_http::p2::default_send_request( + request, config, + )) } } @@ -96,8 +104,10 @@ fn store(engine: &Engine, server: &Server) -> Store { http: WasiHttpCtx::new(), stderr, stdout, - send_request: None, - rejected_authority: None, + hooks: MyHttpHooks { + send_request: None, + rejected_authority: None, + }, }; Store::new(&engine, ctx) @@ -153,8 +163,10 @@ async fn run_wasi_http( http, stderr, stdout, - send_request, - rejected_authority, + hooks: MyHttpHooks { + send_request, + rejected_authority, + }, }; let mut store = Store::new(&engine, ctx); @@ -167,12 +179,14 @@ async fn run_wasi_http( let req = store .data_mut() + .http() .new_incoming_request(Scheme::Http, req) .context("new incoming request")?; let (sender, receiver) = tokio::sync::oneshot::channel(); let out = store .data_mut() + .http() .new_response_outparam(sender) .context("new response outparam")?; diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs index d8e161385b86..fd7f67e3073e 100644 --- a/crates/wasi-http/tests/all/p3/mod.rs +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -19,20 +19,20 @@ use wasmtime::component::{Component, Linker, ResourceTable}; use wasmtime::{Result, Store, ToWasmtimeResult as _, error::Context as _, format_err}; use wasmtime_wasi::p3::bindings::Command; use wasmtime_wasi::{TrappableError, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; -use wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS; use wasmtime_wasi_http::p3::bindings::Service; use wasmtime_wasi_http::p3::bindings::http::types::ErrorCode; use wasmtime_wasi_http::p3::{ - self, Request, RequestOptions, WasiHttpCtx, WasiHttpCtxView, WasiHttpView, + self, Request, RequestOptions, WasiHttpCtxView, WasiHttpHooks, WasiHttpView, }; +use wasmtime_wasi_http::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; foreach_p3_http!(assert_test_exists); -struct TestHttpCtx { +struct TestHooks { request_body_tx: Option>>, } -impl WasiHttpCtx for TestHttpCtx { +impl WasiHttpHooks for TestHooks { fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { name.as_str() == "custom-forbidden-header" || DEFAULT_FORBIDDEN_HEADERS.contains(name) } @@ -83,7 +83,8 @@ impl WasiHttpCtx for TestHttpCtx { struct Ctx { table: ResourceTable, wasi: WasiCtx, - http: TestHttpCtx, + http: WasiHttpCtx, + hooks: TestHooks, } impl Ctx { @@ -91,7 +92,8 @@ impl Ctx { Self { table: ResourceTable::default(), wasi: WasiCtxBuilder::new().inherit_stdio().build(), - http: TestHttpCtx { + http: WasiHttpCtx::new(), + hooks: TestHooks { request_body_tx: Some(request_body_tx), }, } @@ -112,6 +114,7 @@ impl WasiHttpView for Ctx { WasiHttpCtxView { ctx: &mut self.http, table: &mut self.table, + hooks: &mut self.hooks, } } } diff --git a/src/commands/run.rs b/src/commands/run.rs index a10211bbd816..c06e37612240 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -5,7 +5,7 @@ allow(irrefutable_let_patterns, unreachable_patterns) )] -use crate::common::{Profile, RunCommon, RunTarget}; +use crate::common::{HttpHooks, Profile, RunCommon, RunTarget}; use clap::Parser; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -182,17 +182,7 @@ impl RunCommand { } } - let host = Host { - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_buffer_chunks: self - .run - .common - .wasi - .http_outgoing_body_buffer_chunks, - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, - ..Default::default() - }; + let host = Host::default(); let mut store = Store::new(&engine, host); self.populate_with_wasi(&mut linker, &mut store, &main)?; @@ -1195,6 +1185,10 @@ impl RunCommand { /// not compatible with `wasi-threads`. #[derive(Default, Clone)] pub struct Host { + limits: StoreLimits, + #[cfg(feature = "profiling")] + guest_profiler: Option>, + // Legacy wasip1 context using `wasi_common`, not set unless opted-in-to // with the CLI. legacy_p1_ctx: Option, @@ -1218,14 +1212,7 @@ pub struct Host { #[cfg(feature = "wasi-http")] wasi_http: Option>, #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_buffer_chunks: Option, - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_chunk_size: Option, - #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] - p3_http: crate::common::DefaultP3Ctx, - limits: StoreLimits, - #[cfg(feature = "profiling")] - guest_profiler: Option>, + wasi_http_hooks: HttpHooks, #[cfg(feature = "wasi-config")] wasi_config: Option>, @@ -1257,32 +1244,26 @@ impl WasiView for Host { #[cfg(feature = "wasi-http")] impl wasmtime_wasi_http::p2::WasiHttpView for Host { - fn ctx(&mut self) -> &mut WasiHttpCtx { + fn http(&mut self) -> wasmtime_wasi_http::p2::WasiHttpCtxView<'_> { let ctx = self.wasi_http.as_mut().unwrap(); - Arc::get_mut(ctx).expect("wasmtime_wasi is not compatible with threads") - } - - fn table(&mut self) -> &mut wasmtime::component::ResourceTable { - WasiView::ctx(self).table - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.wasi_http_outgoing_body_buffer_chunks - .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.wasi_http_outgoing_body_chunk_size - .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE) + let ctx = Arc::get_mut(ctx).expect("wasmtime_wasi_http is not compatible with threads"); + wasmtime_wasi_http::p2::WasiHttpCtxView { + table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, + ctx, + hooks: &mut self.wasi_http_hooks, + } } } #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] impl wasmtime_wasi_http::p3::WasiHttpView for Host { fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> { + let ctx = self.wasi_http.as_mut().unwrap(); + let ctx = Arc::get_mut(ctx).expect("wasmtime_wasi_http is not compatible with threads"); wasmtime_wasi_http::p3::WasiHttpCtxView { table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, - ctx: &mut self.p3_http, + ctx, + hooks: &mut self.wasi_http_hooks, } } } diff --git a/src/commands/serve.rs b/src/commands/serve.rs index bb53c52b2ce3..4badbe845734 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,4 +1,4 @@ -use crate::common::{Profile, RunCommon, RunTarget}; +use crate::common::{HttpHooks, Profile, RunCommon, RunTarget}; use bytes::Bytes; use clap::Parser; use futures::future::FutureExt; @@ -20,7 +20,7 @@ use std::{ }; use tokio::io::{self, AsyncWrite}; use tokio::sync::Notify; -use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime::component::{Component, Linker}; use wasmtime::{ Engine, Result, Store, StoreContextMut, StoreLimits, UpdateDeadline, bail, error::Context as _, }; @@ -48,11 +48,7 @@ struct Host { table: wasmtime::component::ResourceTable, ctx: WasiCtx, http: WasiHttpCtx, - http_outgoing_body_buffer_chunks: Option, - http_outgoing_body_chunk_size: Option, - - #[cfg(feature = "component-model-async")] - p3_http: crate::common::DefaultP3Ctx, + hooks: HttpHooks, limits: StoreLimits, @@ -78,22 +74,13 @@ impl WasiView for Host { } } -impl WasiHttpView for Host { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http - } - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.http_outgoing_body_buffer_chunks - .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.http_outgoing_body_chunk_size - .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE) +impl wasmtime_wasi_http::p2::WasiHttpView for Host { + fn http(&mut self) -> wasmtime_wasi_http::p2::WasiHttpCtxView<'_> { + wasmtime_wasi_http::p2::WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.hooks, + } } } @@ -102,7 +89,8 @@ impl wasmtime_wasi_http::p3::WasiHttpView for Host { fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> { wasmtime_wasi_http::p3::WasiHttpCtxView { table: &mut self.table, - ctx: &mut self.p3_http, + ctx: &mut self.http, + hooks: &mut self.hooks, } } } @@ -237,8 +225,7 @@ impl ServeCommand { table, ctx: builder.build(), http: self.run.wasi_http_ctx()?, - http_outgoing_body_buffer_chunks: self.run.common.wasi.http_outgoing_body_buffer_chunks, - http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, + hooks: self.run.wasi_http_hooks(), limits: StoreLimits::default(), @@ -250,8 +237,6 @@ impl ServeCommand { wasi_keyvalue: None, #[cfg(feature = "profiling")] guest_profiler: None, - #[cfg(feature = "component-model-async")] - p3_http: crate::common::DefaultP3Ctx, }; if self.run.common.wasi.nn == Some(true) { @@ -892,8 +877,9 @@ async fn handle_request( let (req, out) = store.with(move |mut store| { let req = store .data_mut() + .http() .new_incoming_request(p2::http::types::Scheme::Http, req)?; - let out = store.data_mut().new_response_outparam(tx)?; + let out = store.data_mut().http().new_response_outparam(tx)?; wasmtime::error::Ok((req, out)) })?; diff --git a/src/common.rs b/src/common.rs index fcaf4c9bfc69..fa7fb061e775 100644 --- a/src/common.rs +++ b/src/common.rs @@ -341,6 +341,22 @@ impl RunCommon { Ok(http) } + #[cfg(feature = "wasi-http")] + pub fn wasi_http_hooks(&self) -> HttpHooks { + HttpHooks { + p2_outgoing_body_buffer_chunks: self + .common + .wasi + .http_outgoing_body_buffer_chunks + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS), + p2_outgoing_body_chunk_size: self + .common + .wasi + .http_outgoing_body_chunk_size + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE), + } + } + pub fn compute_preopen_sockets(&self) -> Result> { let mut listeners = vec![]; @@ -444,8 +460,34 @@ impl Profile { } } -#[derive(Default, Clone)] -#[cfg(all(feature = "wasi-http", feature = "component-model-async"))] -pub struct DefaultP3Ctx; -#[cfg(all(feature = "wasi-http", feature = "component-model-async"))] -impl wasmtime_wasi_http::p3::WasiHttpCtx for DefaultP3Ctx {} +#[derive(Copy, Clone, Debug)] +#[cfg(feature = "wasi-http")] +pub struct HttpHooks { + p2_outgoing_body_buffer_chunks: usize, + p2_outgoing_body_chunk_size: usize, +} + +#[cfg(feature = "wasi-http")] +impl Default for HttpHooks { + fn default() -> Self { + Self { + p2_outgoing_body_buffer_chunks: + wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, + p2_outgoing_body_chunk_size: wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE, + } + } +} + +#[cfg(feature = "wasi-http")] +impl wasmtime_wasi_http::p2::WasiHttpHooks for HttpHooks { + fn outgoing_body_buffer_chunks(&mut self) -> usize { + self.p2_outgoing_body_buffer_chunks + } + + fn outgoing_body_chunk_size(&mut self) -> usize { + self.p2_outgoing_body_chunk_size + } +} + +#[cfg(feature = "wasi-http")] +impl wasmtime_wasi_http::p3::WasiHttpHooks for HttpHooks {} From 66d0dd13bb097390c5dbe8ce7d1ce9afa8fde663 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 14:25:01 -0700 Subject: [PATCH 3/7] Make `WasiHttp` in WASIp2 public --- crates/wasi-http/src/p2/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/wasi-http/src/p2/mod.rs b/crates/wasi-http/src/p2/mod.rs index e0bdfb9c743a..25dd423f1643 100644 --- a/crates/wasi-http/src/p2/mod.rs +++ b/crates/wasi-http/src/p2/mod.rs @@ -357,7 +357,8 @@ pub struct WasiHttpCtxView<'a> { pub hooks: &'a mut dyn WasiHttpHooks, } -struct WasiHttp; +/// The type for which this crate implements the `wasi:http` interfaces. +pub struct WasiHttp; impl HasData for WasiHttp { type Data<'a> = WasiHttpCtxView<'a>; From 6983a9b028c70ea27c4f4d9b257d80d0f8a1f157 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 14:55:53 -0700 Subject: [PATCH 4/7] Fix some conditional build --- Cargo.toml | 2 ++ crates/wasi-http/src/handler.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 5eb62d5956fa..7e5d171bfb1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -576,6 +576,7 @@ serve = [ "dep:http-body-util", "dep:http", "wasmtime-cli-flags/async", + "wasmtime-wasi-http?/p2", ] explore = ["dep:wasmtime-explorer", "dep:tempfile"] wast = ["dep:wasmtime-wast"] @@ -589,6 +590,7 @@ run = [ "dep:wasi-common", "dep:tokio", "wasmtime-cli-flags/async", + "wasmtime-wasi-http?/p2", ] completion = ["dep:clap_complete"] objdump = [ diff --git a/crates/wasi-http/src/handler.rs b/crates/wasi-http/src/handler.rs index 9b08c0b19bdd..ce646a78a560 100644 --- a/crates/wasi-http/src/handler.rs +++ b/crates/wasi-http/src/handler.rs @@ -24,6 +24,7 @@ use wasmtime::{Result, Store, StoreContextMut, format_err}; /// Alternative p2 bindings generated with `exports: { default: async | store }` /// so we can use `TypedFunc::call_concurrent` with both p2 and p3 instances. +#[cfg(feature = "p2")] pub mod p2 { #[expect(missing_docs, reason = "bindgen-generated code")] pub mod bindings { @@ -49,6 +50,7 @@ pub mod p2 { /// `wasi:http/handler@0.3.x` pre-instance. pub enum ProxyPre { /// A `wasi:http/incoming-handler@0.2.x` pre-instance. + #[cfg(feature = "p2")] P2(p2::bindings::ProxyPre), /// A `wasi:http/handler@0.3.x` pre-instance. #[cfg(feature = "p3")] @@ -61,6 +63,7 @@ impl ProxyPre { T: Send, { Ok(match self { + #[cfg(feature = "p2")] Self::P2(pre) => Proxy::P2(pre.instantiate_async(store).await?), #[cfg(feature = "p3")] Self::P3(pre) => Proxy::P3(pre.instantiate_async(store).await?), @@ -72,6 +75,7 @@ impl ProxyPre { /// `wasi:http/handler@0.3.x` instance. pub enum Proxy { /// A `wasi:http/incoming-handler@0.2.x` instance. + #[cfg(feature = "p2")] P2(p2::bindings::Proxy), /// A `wasi:http/handler@0.3.x` instance. #[cfg(feature = "p3")] From 00381a660d3c633c80d37b070c3b46754381592b Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 14:57:34 -0700 Subject: [PATCH 5/7] Fix some doctests --- crates/wasi-http/src/p3/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index 7af0829cff23..bce694348e59 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -198,7 +198,7 @@ pub trait WasiHttpView: Send { /// ``` /// use wasmtime::{Engine, Result, Store, Config}; /// use wasmtime::component::{Linker, ResourceTable}; -/// use wasmtime_wasi_http::p3::{DefaultWasiHttpCtx, WasiHttpCtxView, WasiHttpView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p3::{WasiHttpCtxView, WasiHttpView}}; /// /// fn main() -> Result<()> { /// let mut config = Config::new(); @@ -221,7 +221,7 @@ pub trait WasiHttpView: Send { /// /// #[derive(Default)] /// struct MyState { -/// http: DefaultWasiHttpCtx, +/// http: WasiHttpCtx, /// table: ResourceTable, /// } /// @@ -230,6 +230,7 @@ pub trait WasiHttpView: Send { /// WasiHttpCtxView { /// ctx: &mut self.http, /// table: &mut self.table, +/// hooks: Default::default(), /// } /// } /// } From 31613a7ed4256d670e498fde5aefbd9c35a02ec5 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 16:12:10 -0700 Subject: [PATCH 6/7] Fix configured build --- src/commands/run.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/run.rs b/src/commands/run.rs index c06e37612240..ffb4f458a09c 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -5,7 +5,7 @@ allow(irrefutable_let_patterns, unreachable_patterns) )] -use crate::common::{HttpHooks, Profile, RunCommon, RunTarget}; +use crate::common::{Profile, RunCommon, RunTarget}; use clap::Parser; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -1212,7 +1212,7 @@ pub struct Host { #[cfg(feature = "wasi-http")] wasi_http: Option>, #[cfg(feature = "wasi-http")] - wasi_http_hooks: HttpHooks, + wasi_http_hooks: crate::common::HttpHooks, #[cfg(feature = "wasi-config")] wasi_config: Option>, From 7c1519c78592a99c9e91e1bfbe0e11ffc20fa4a4 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 10 Mar 2026 09:10:16 -0700 Subject: [PATCH 7/7] Fixup documentation --- crates/wasi-http/src/p2/mod.rs | 34 +++++++++++++++++++------------- crates/wasi-http/src/p2/types.rs | 4 ++-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/wasi-http/src/p2/mod.rs b/crates/wasi-http/src/p2/mod.rs index 25dd423f1643..6f42d9260c26 100644 --- a/crates/wasi-http/src/p2/mod.rs +++ b/crates/wasi-http/src/p2/mod.rs @@ -14,25 +14,31 @@ //! //! The crate also contains an implementation of the [`wasi:http/proxy`] world. //! -//! [`wasi:http/proxy`]: crate::bindings::Proxy -//! [`wasi:http/outgoing-handler`]: crate::bindings::http::outgoing_handler::Host -//! [`wasi:http/types`]: crate::bindings::http::types::Host -//! [`wasi:http/incoming-handler`]: crate::bindings::exports::wasi::http::incoming_handler::Guest +//! [`wasi:http/proxy`]: crate::p2::bindings::Proxy +//! [`wasi:http/outgoing-handler`]: crate::p2::bindings::http::outgoing_handler::Host +//! [`wasi:http/types`]: crate::p2::bindings::http::types::Host +//! [`wasi:http/incoming-handler`]: crate::p2::bindings::exports::wasi::http::incoming_handler::Guest //! //! This crate is very similar to [`wasmtime_wasi`] in the it uses the //! `bindgen!` macro in Wasmtime to generate bindings to interfaces. Bindings //! are located in the [`bindings`] module. //! -//! # The `WasiHttpView` trait +//! # The `WasiHttp{View,Hooks}` traits //! -//! All `bindgen!`-generated `Host` traits are implemented in terms of a -//! [`WasiHttpView`] trait which provides basic access to [`WasiHttpCtx`], -//! configuration for WASI HTTP, and a [`wasmtime_wasi::ResourceTable`], the -//! state for all host-defined component model resources. +//! All `bindgen!`-generated `Host` traits are implemented for the +//! [`WasiHttpCtxView`] type. This type is created from a store's data `T` +//! through the [`WasiHttpView`] trait. The [`add_to_linker_async`] function, +//! for example, uses [`WasiHttpView`] to acquire the context view. //! -//! The [`WasiHttpView`] trait additionally offers a few other configuration -//! methods such as [`WasiHttpView::send_request`] to customize how outgoing -//! HTTP requests are handled. +//! The [`WasiHttpCtxView`] structure requires that a [`ResourceTable`] and +//! [`WasiHttpCtx`] live within the store. This is store-specific state that is +//! used to implement various APIs and store host state. +//! +//! The final `hooks` field within [`WasiHttpCtxView`] is a trait object of +//! [`WasiHttpHooks`]. This provides a few more hooks, dynamically, to configure +//! how `wasi:http` behaves. For example [`WasiHttpHooks::send_request`] can +//! customize how outgoing HTTP requests are handled. The `hooks` field can be +//! initialized with the [`default_hooks`] function for the default behavior. //! //! # Async and Sync //! @@ -335,9 +341,9 @@ pub fn default_hooks() -> &'static mut dyn WasiHttpHooks { Default::default() } -/// The default value configured for [`WasiHttpView::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. +/// The default value configured for [`WasiHttpHooks::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. pub const DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS: usize = 1; -/// The default value configured for [`WasiHttpView::outgoing_body_chunk_size`] in [`WasiHttpView`]. +/// The default value configured for [`WasiHttpHooks::outgoing_body_chunk_size`] in [`WasiHttpView`]. pub const DEFAULT_OUTGOING_BODY_CHUNK_SIZE: usize = 1024 * 1024; /// Structure which `wasi:http` `Host`-style traits are implemented for. diff --git a/crates/wasi-http/src/p2/types.rs b/crates/wasi-http/src/p2/types.rs index fcdf2a45bdaa..d53290acd306 100644 --- a/crates/wasi-http/src/p2/types.rs +++ b/crates/wasi-http/src/p2/types.rs @@ -1,5 +1,5 @@ -//! Implements the base structure (i.e. [WasiHttpCtx]) that will provide the -//! implementation of the wasi-http API. +//! Implements the base structure that will provide the implementation of the +//! wasi-http API. use crate::p2::{ WasiHttpCtxView, WasiHttpHooks,