Repository: bytecodealliance/wasmtime
Pull request #6974 (2023-09-06 23:55:47)

chore: backport 6878 to release-13.0.0
Backporting #6878 to release v13.0.0 branch.

eduardomourar: @alexcrichton, I just cherry picked the commit and resolved a few conflicts. I hope this is enough.
github-actions[bot]: #### Subscribe to Label Action

cc @peterhuene

<details>
This issue or pull request has been labeled: "wasi", "wasmtime:api", "wasmtime:c-api"

Thus the following users have been cc'd because of the following labels:

* peterhuene: wasmtime:api, wasmtime:c-api

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6c59eeef0..1ffa39433 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1223,7 +1223,6 @@ checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549"
 dependencies = [
  "futures-channel",
  "futures-core",
- "futures-executor",
  "futures-io",
  "futures-sink",
  "futures-task",
@@ -1246,17 +1245,6 @@ version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
 
-[[package]]
-name = "futures-executor"
-version = "0.3.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
 [[package]]
 name = "futures-io"
 version = "0.3.28"
@@ -1281,15 +1269,11 @@ version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
 dependencies = [
- "futures-channel",
  "futures-core",
- "futures-io",
  "futures-sink",
  "futures-task",
- "memchr",
  "pin-project-lite",
  "pin-utils",
- "slab",
 ]
 
 [[package]]
@@ -3812,6 +3796,7 @@ dependencies = [
  "thiserror",
  "tokio",
  "tokio-rustls",
+ "tracing",
  "wasmtime",
  "wasmtime-wasi",
  "webpki-roots",
diff --git a/Cargo.toml b/Cargo.toml
index 5d33bfc78..a12c80ddc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,7 +29,9 @@ wasmtime-cranelift = { workspace = true }
 wasmtime-environ = { workspace = true }
 wasmtime-explorer = { workspace = true }
 wasmtime-wast = { workspace = true }
-wasmtime-wasi = { workspace = true, features = ["exit"] }
+wasmtime-wasi = { workspace = true, default-features = true, features = [
+  "exit",
+] }
 wasmtime-wasi-nn = { workspace = true, optional = true }
 wasmtime-wasi-threads = { workspace = true, optional = true }
 wasmtime-wasi-http = { workspace = true, optional = true }
@@ -143,8 +145,8 @@ wasmtime-jit = { path = "crates/jit", version = "=13.0.0" }
 wasmtime-jit-debug = { path = "crates/jit-debug", version = "=13.0.0" }
 wasmtime-runtime = { path = "crates/runtime", version = "=13.0.0" }
 wasmtime-wast = { path = "crates/wast", version = "=13.0.0" }
-wasmtime-wasi = { path = "crates/wasi", version = "13.0.0" }
-wasmtime-wasi-http = { path = "crates/wasi-http", version = "=13.0.0" }
+wasmtime-wasi = { path = "crates/wasi", version = "13.0.0", default-features = false }
+wasmtime-wasi-http = { path = "crates/wasi-http", version = "=13.0.0", default-features = false }
 wasmtime-wasi-nn = { path = "crates/wasi-nn", version = "13.0.0" }
 wasmtime-wasi-threads = { path = "crates/wasi-threads", version = "13.0.0" }
 wasmtime-component-util = { path = "crates/component-util", version = "=13.0.0" }
@@ -272,7 +274,7 @@ jitdump = ["wasmtime/jitdump"]
 vtune = ["wasmtime/vtune"]
 wasi-nn = ["dep:wasmtime-wasi-nn"]
 wasi-threads = ["dep:wasmtime-wasi-threads"]
-wasi-http = ["dep:wasmtime-wasi-http"]
+wasi-http = ["dep:wasmtime-wasi-http", "wasmtime-wasi-http?/sync"]
 pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"]
 all-arch = ["wasmtime/all-arch"]
 component-model = [
diff --git a/crates/bench-api/Cargo.toml b/crates/bench-api/Cargo.toml
index 3a617b821..138a9d27a 100644
--- a/crates/bench-api/Cargo.toml
+++ b/crates/bench-api/Cargo.toml
@@ -21,7 +21,7 @@ shuffling-allocator = { version = "1.1.1", optional = true }
 target-lexicon = { workspace = true }
 wasmtime = { workspace = true }
 wasmtime-cli-flags = { workspace = true, default-features = true }
-wasmtime-wasi = { workspace = true }
+wasmtime-wasi = { workspace = true, default-features = true }
 wasmtime-wasi-nn = { workspace = true, optional = true }
 wasi-cap-std-sync = { workspace = true }
 cap-std = { workspace = true }
diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml
index a464c0dbd..26e358424 100644
--- a/crates/c-api/Cargo.toml
+++ b/crates/c-api/Cargo.toml
@@ -28,7 +28,7 @@ wat = { workspace = true, optional = true }
 
 # Optional dependencies for the `wasi` feature
 wasi-cap-std-sync = { workspace = true, optional = true }
-wasmtime-wasi = { workspace = true, optional = true }
+wasmtime-wasi = { workspace = true, default-features = true, optional = true }
 cap-std = { workspace = true, optional = true }
 wasi-common = { workspace = true, optional = true }
 
diff --git a/crates/wasi-http/Cargo.toml b/crates/wasi-http/Cargo.toml
index f0ce143b8..3ebbea483 100644
--- a/crates/wasi-http/Cargo.toml
+++ b/crates/wasi-http/Cargo.toml
@@ -11,9 +11,7 @@ description = "Experimental HTTP library for WebAssembly in Wasmtime"
 anyhow = { workspace = true }
 async-trait = { workspace = true }
 bytes = { workspace = true }
-futures = { workspace = true, default-features = false, features = [
-    "executor",
-] }
+futures = { workspace = true, default-features = false }
 hyper = { version = "=1.0.0-rc.3", features = ["full"] }
 tokio = { version = "1", default-features = false, features = [
     "net",
@@ -24,7 +22,10 @@ http = { version = "0.2.9" }
 http-body = "1.0.0-rc.2"
 http-body-util = "0.1.0-rc.2"
 thiserror = { workspace = true }
-wasmtime-wasi = { workspace = true }
+tracing = { workspace = true }
+wasmtime-wasi = { workspace = true, default-features = false, features = [
+    "preview2",
+] }
 wasmtime = { workspace = true, features = ['component-model'] }
 
 # The `ring` crate, used to implement TLS, does not build on riscv64 or s390x
@@ -32,3 +33,7 @@ wasmtime = { workspace = true, features = ['component-model'] }
 tokio-rustls = { version = "0.24.0" }
 rustls = { version = "0.21.6" }
 webpki-roots = { version = "0.25.2" }
+
+[features]
+default = ["sync"]
+sync = ["wasmtime-wasi/sync"]
diff --git a/crates/wasi-http/src/component_impl.rs b/crates/wasi-http/src/component_impl.rs
index b660d1f2a..3ffc0e0f2 100644
--- a/crates/wasi-http/src/component_impl.rs
+++ b/crates/wasi-http/src/component_impl.rs
@@ -1,5 +1,8 @@
-use crate::wasi::http::types::{Error, Method, RequestOptions, Scheme};
-use crate::{WasiHttpView, WasiHttpViewExt};
+use crate::bindings::http::{
+    outgoing_handler,
+    types::{Error, Host, Method, RequestOptions, Scheme},
+};
+use crate::WasiHttpView;
 use anyhow::anyhow;
 use std::str;
 use std::vec::Vec;
@@ -107,18 +110,10 @@ fn u32_array_to_u8(arr: &[u32]) -> Vec<u8> {
     result
 }
 
-pub fn add_component_to_linker<T>(
+pub fn add_component_to_linker<T: WasiHttpView>(
     linker: &mut wasmtime::Linker<T>,
     get_cx: impl Fn(&mut T) -> &mut T + Send + Sync + Copy + 'static,
-) -> anyhow::Result<()>
-where
-    T: WasiHttpView
-        + WasiHttpViewExt
-        + crate::wasi::http::outgoing_handler::Host
-        + crate::wasi::http::types::Host
-        + io::streams::Host
-        + poll::poll::Host,
-{
+) -> anyhow::Result<()> {
     linker.func_wrap8_async(
         "wasi:http/outgoing-handler",
         "handle",
@@ -154,7 +149,14 @@ where
                     None
                 };
 
-                get_cx(caller.data_mut()).handle(request, options).await
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!("[module='wasi:http/outgoing-handler' function='handle'] call request={:?} options={:?}", request, options);
+                let result = outgoing_handler::Host::handle(ctx, request, options).await;
+                tracing::trace!(
+                    "[module='wasi:http/outgoing-handler' function='handle'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -193,9 +195,9 @@ where
                     authority_len,
                 )?;
 
-                let mut s = Scheme::Https;
+                let mut s = Some(Scheme::Https);
                 if scheme_is_some == 1 {
-                    s = match scheme {
+                    s = Some(match scheme {
                         0 => Scheme::Http,
                         1 => Scheme::Https,
                         _ => {
@@ -207,7 +209,7 @@ where
                             )?;
                             Scheme::Other(value)
                         }
-                    };
+                    });
                 }
                 let m = match method {
                     0 => Method::Get,
@@ -231,8 +233,21 @@ where
                 };
 
                 let ctx = get_cx(caller.data_mut());
-                ctx.new_outgoing_request(m, path, Some(s), authority, headers)
-                    .await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-outgoing-request'] call method={:?} path={:?} scheme={:?} authority={:?} headers={:?}",
+                    m,
+                    path,
+                    s,
+                    authority,
+                    headers
+                );
+                let result =
+                    Host::new_outgoing_request(ctx, m, path, s, authority, headers).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-outgoing-request'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -242,18 +257,35 @@ where
         move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                let result: u32 = ctx.incoming_response_status(id).await?.into();
-                Ok(result)
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-status'] call id={:?}",
+                    id
+                );
+                let result = Ok(u32::from(Host::incoming_response_status(ctx, id).await?));
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-status'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
     linker.func_wrap1_async(
         "wasi:http/types",
         "drop-future-incoming-response",
-        move |mut caller: Caller<'_, T>, future: u32| {
+        move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                ctx.drop_future_incoming_response(future).await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-future-incoming-response'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_future_incoming_response(ctx, id).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-future-incoming-response'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -263,7 +295,16 @@ where
         move |mut caller: Caller<'_, T>, future: u32, ptr: i32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                let response = ctx.future_incoming_response_get(future).await?;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='future-incoming-response-get'] call future={:?}",
+                    future
+                );
+                let result = Host::future_incoming_response_get(ctx, future).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='future-incoming-response-get'] return result={:?}",
+                    result
+                );
+                let response = result?;
 
                 let memory = memory_get(&mut caller)?;
 
@@ -304,7 +345,16 @@ where
         move |mut caller: Caller<'_, T>, future: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                ctx.listen_to_future_incoming_response(future).await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='listen-to-future-incoming-response'] call future={:?}",
+                    future
+                );
+                let result = Host::listen_to_future_incoming_response(ctx, future).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='listen-to-future-incoming-response'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -314,16 +364,21 @@ where
         move |mut caller: Caller<'_, T>, response: u32, ptr: i32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                let stream = ctx.incoming_response_consume(response).await?.unwrap_or(0);
+                    tracing::trace!(
+                        "[module='wasi:http/types' function='incoming-response-consume'] call response={:?}",
+                        response
+                    );
+                    let result = Host::incoming_response_consume(ctx, response).await;
+                    tracing::trace!(
+                        "[module='wasi:http/types' function='incoming-response-consume'] return result={:?}",
+                        result
+                    );
+                    let stream = result?.unwrap_or(0);
 
                 let memory = memory_get(&mut caller).unwrap();
 
                 // First == is_some
                 // Second == stream_id
-                // let result: [u32; 2] = match result {
-                //     Ok(value) => [0, value],
-                //     Err(_) => [1, 0],
-                // };
                 let result: [u32; 2] = [0, stream];
                 let raw = u32_array_to_u8(&result);
 
@@ -338,7 +393,16 @@ where
         move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                poll::poll::Host::drop_pollable(ctx, id).await
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='drop-pollable'] call id={:?}",
+                    id
+                );
+                let result = poll::poll::Host::drop_pollable(ctx, id).await;
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='drop-pollable'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -359,7 +423,16 @@ where
                 }
 
                 let ctx = get_cx(caller.data_mut());
-                let result = poll::poll::Host::poll_oneoff(ctx, vec).await?;
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='poll-oneoff'] call in={:?}",
+                    vec
+                );
+                let result = poll::poll::Host::poll_oneoff(ctx, vec).await;
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='poll-oneoff'] return result={:?}",
+                    result
+                );
+                let result = result?;
 
                 let result_len = result.len();
                 let result_ptr =
@@ -388,7 +461,16 @@ where
         move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                io::streams::Host::drop_input_stream(ctx, id).await
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-input-stream'] call id={:?}",
+                    id
+                );
+                let result = io::streams::Host::drop_input_stream(ctx, id).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-input-stream'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -398,7 +480,16 @@ where
         move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                io::streams::Host::drop_output_stream(ctx, id).await
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-output-stream'] call id={:?}",
+                    id
+                );
+                let result = io::streams::Host::drop_output_stream(ctx, id).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-output-stream'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -408,10 +499,56 @@ where
         move |mut caller: Caller<'_, T>, stream: u32, len: u64, ptr: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='read'] call this={:?} len={:?}",
+                    stream,
+                    len
+                );
+                let result = io::streams::Host::read(ctx, stream, len).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='read'] return result={:?}",
+                    result
+                );
+                let (bytes, status) = result?.map_err(|_| anyhow!("read failed"))?;
+
+                let done = match status {
+                    io::streams::StreamStatus::Open => 0,
+                    io::streams::StreamStatus::Ended => 1,
+                };
+                let body_len: u32 = bytes.len().try_into()?;
+                let out_ptr = allocate_guest_pointer(&mut caller, body_len).await?;
 
-                let (bytes, status) = io::streams::Host::read(ctx, stream, len)
-                    .await?
-                    .map_err(|_| anyhow!("read failed"))?;
+                // First == is_err
+                // Second == {ok: is_err = false, tag: is_err = true}
+                // Third == bytes length
+                // Fourth == enum status
+                let result: [u32; 4] = [0, out_ptr, body_len, done];
+                let raw = u32_array_to_u8(&result);
+
+                let memory = memory_get(&mut caller)?;
+                memory.write(caller.as_context_mut(), out_ptr as _, &bytes)?;
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+                Ok(())
+            })
+        },
+    )?;
+    linker.func_wrap3_async(
+        "wasi:io/streams",
+        "blocking-read",
+        move |mut caller: Caller<'_, T>, stream: u32, len: u64, ptr: u32| {
+            Box::new(async move {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-read'] call this={:?} len={:?}",
+                    stream,
+                    len
+                );
+                let result = io::streams::Host::blocking_read(ctx, stream, len).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-read'] return result={:?}",
+                    result
+                );
+                let (bytes, status) = result?.map_err(|_| anyhow!("read failed"))?;
 
                 let done = match status {
                     io::streams::StreamStatus::Open => 0,
@@ -440,7 +577,16 @@ where
         move |mut caller: Caller<'_, T>, stream: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                io::streams::Host::subscribe_to_input_stream(ctx, stream).await
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-input-stream'] call stream={:?}",
+                    stream
+                );
+                let result = io::streams::Host::subscribe_to_input_stream(ctx, stream).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-input-stream'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -450,7 +596,16 @@ where
         move |mut caller: Caller<'_, T>, stream: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                io::streams::Host::subscribe_to_output_stream(ctx, stream).await
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-output-stream'] call stream={:?}",
+                    stream
+                );
+                let result = io::streams::Host::subscribe_to_output_stream(ctx, stream).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-output-stream'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -459,15 +614,64 @@ where
         "write",
         move |mut caller: Caller<'_, T>, stream: u32, body_ptr: u32, body_len: u32, ptr: u32| {
             Box::new(async move {
-                let memory: Memory = memory_get(&mut caller)?;
+                let memory = memory_get(&mut caller)?;
                 let body =
                     string_from_memory(&memory, caller.as_context_mut(), body_ptr, body_len)?;
 
                 let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='write'] call stream={:?} body={:?}",
+                    stream,
+                    body
+                );
+                let result = io::streams::Host::write(ctx, stream, body.into()).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='write'] return result={:?}",
+                    result
+                );
+                let (len, status) = result?.map_err(|_| anyhow!("write failed"))?;
+
+                let written: u32 = len.try_into()?;
+                let done: u32 = match status {
+                    io::streams::StreamStatus::Open => 0,
+                    io::streams::StreamStatus::Ended => 1,
+                };
+
+                // First == is_err
+                // Second == {ok: is_err = false, tag: is_err = true}
+                // Third == amount of bytes written
+                // Fifth == enum status
+                let result: [u32; 5] = [0, 0, written, 0, done];
+                let raw = u32_array_to_u8(&result);
+
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+
+                Ok(())
+            })
+        },
+    )?;
+    linker.func_wrap4_async(
+        "wasi:io/streams",
+        "blocking-write",
+        move |mut caller: Caller<'_, T>, stream: u32, body_ptr: u32, body_len: u32, ptr: u32| {
+            Box::new(async move {
+                let memory = memory_get(&mut caller)?;
+                let body =
+                    string_from_memory(&memory, caller.as_context_mut(), body_ptr, body_len)?;
+
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-write'] call stream={:?} body={:?}",
+                    stream,
+                    body
+                );
+                let result = io::streams::Host::blocking_write(ctx, stream, body.into()).await;
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-write'] return result={:?}",
+                    result
+                );
+                let (len, status) = result?.map_err(|_| anyhow!("write failed"))?;
 
-                let (len, status) = io::streams::Host::write(ctx, stream, body.into())
-                    .await?
-                    .map_err(|_| anyhow!("write failed"))?;
                 let written: u32 = len.try_into()?;
                 let done: u32 = match status {
                     io::streams::StreamStatus::Open => 0,
@@ -490,10 +694,19 @@ where
     linker.func_wrap1_async(
         "wasi:http/types",
         "drop-fields",
-        move |mut caller: Caller<'_, T>, ptr: u32| {
+        move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                ctx.drop_fields(ptr).await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-fields'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_fields(ctx, id).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-fields'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -503,9 +716,16 @@ where
         move |mut caller: Caller<'_, T>, request: u32, ptr: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                let stream = ctx
-                    .outgoing_request_write(request)
-                    .await?
+                tracing::trace!(
+                    "[module='wasi:http/types' function='outgoing-request-write'] call request={:?}",
+                    request
+                );
+                let result = Host::outgoing_request_write(ctx, request).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='outgoing-request-write'] return result={:?}",
+                    result
+                );
+                let stream = result?
                     .map_err(|_| anyhow!("no outgoing stream present"))?;
 
                 let memory = memory_get(&mut caller)?;
@@ -525,7 +745,16 @@ where
         move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                ctx.drop_outgoing_request(id).await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-outgoing-request'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_outgoing_request(ctx, id).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-outgoing-request'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -535,7 +764,16 @@ where
         move |mut caller: Caller<'_, T>, id: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                ctx.drop_incoming_response(id).await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-incoming-response'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_incoming_response(ctx, id).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-incoming-response'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -566,7 +804,16 @@ where
                 }
 
                 let ctx = get_cx(caller.data_mut());
-                ctx.new_fields(vec).await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-fields'] call entries={:?}",
+                    vec
+                );
+                let result = Host::new_fields(ctx, vec).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-fields'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
@@ -576,7 +823,16 @@ where
         move |mut caller: Caller<'_, T>, fields: u32, out_ptr: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                let entries = ctx.fields_entries(fields).await?;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='fields-entries'] call fields={:?}",
+                    fields
+                );
+                let result = Host::fields_entries(ctx, fields).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='fields-entries'] return result={:?}",
+                    result
+                );
+                let entries = result?;
 
                 let header_len = entries.len();
                 let tuple_ptr =
@@ -616,9 +872,814 @@ where
         move |mut caller: Caller<'_, T>, handle: u32| {
             Box::new(async move {
                 let ctx = get_cx(caller.data_mut());
-                ctx.incoming_response_headers(handle).await
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-headers'] call handle={:?}",
+                    handle
+                );
+                let result = Host::incoming_response_headers(ctx, handle).await;
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-headers'] return result={:?}",
+                    result
+                );
+                result
             })
         },
     )?;
     Ok(())
 }
+
+pub mod sync {
+    use super::{
+        memory_get, read_option_string, string_from_memory, u32_array_to_u8, u32_from_memory,
+    };
+    use crate::bindings::sync::http::{
+        outgoing_handler,
+        types::{Error, Host, Method, RequestOptions, Scheme},
+    };
+    use crate::WasiHttpView;
+    use anyhow::anyhow;
+    use wasmtime::{AsContext, AsContextMut, Caller};
+    use wasmtime_wasi::preview2::bindings::sync_io::{io, poll};
+
+    fn allocate_guest_pointer<T: Send>(
+        caller: &mut Caller<'_, T>,
+        size: u32,
+    ) -> anyhow::Result<u32> {
+        let realloc = caller
+            .get_export("cabi_realloc")
+            .ok_or_else(|| anyhow!("missing required export cabi_realloc"))?;
+        let func = realloc
+            .into_func()
+            .ok_or_else(|| anyhow!("cabi_realloc must be a func"))?;
+        let typed = func.typed::<(u32, u32, u32, u32), u32>(caller.as_context())?;
+        Ok(typed.call(caller.as_context_mut(), (0, 0, 4, size))?)
+    }
+
+    pub fn add_component_to_linker<T: WasiHttpView>(
+        linker: &mut wasmtime::Linker<T>,
+        get_cx: impl Fn(&mut T) -> &mut T + Send + Sync + Copy + 'static,
+    ) -> anyhow::Result<()> {
+        linker.func_wrap(
+            "wasi:http/outgoing-handler",
+            "handle",
+            move |mut caller: Caller<'_, T>,
+                  request: u32,
+                  has_options: i32,
+                  has_timeout: i32,
+                  timeout_ms: u32,
+                  has_first_byte_timeout: i32,
+                  first_byte_timeout_ms: u32,
+                  has_between_bytes_timeout: i32,
+                  between_bytes_timeout_ms: u32|
+                  -> anyhow::Result<u32> {
+                let options = if has_options == 1 {
+                    Some(RequestOptions {
+                        connect_timeout_ms: if has_timeout == 1 {
+                            Some(timeout_ms)
+                        } else {
+                            None
+                        },
+                        first_byte_timeout_ms: if has_first_byte_timeout == 1 {
+                            Some(first_byte_timeout_ms)
+                        } else {
+                            None
+                        },
+                        between_bytes_timeout_ms: if has_between_bytes_timeout == 1 {
+                            Some(between_bytes_timeout_ms)
+                        } else {
+                            None
+                        },
+                    })
+                } else {
+                    None
+                };
+
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!("[module='wasi:http/outgoing-handler' function='handle'] call request={:?} options={:?}", request, options);
+                let result = outgoing_handler::Host::handle(ctx, request, options);
+                tracing::trace!(
+                    "[module='wasi:http/outgoing-handler' function='handle'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "new-outgoing-request",
+            move |mut caller: Caller<'_, T>,
+                  method: i32,
+                  method_ptr: i32,
+                  method_len: i32,
+                  path_is_some: i32,
+                  path_ptr: u32,
+                  path_len: u32,
+                  scheme_is_some: i32,
+                  scheme: i32,
+                  scheme_ptr: i32,
+                  scheme_len: i32,
+                  authority_is_some: i32,
+                  authority_ptr: u32,
+                  authority_len: u32,
+                  headers: u32|
+                  -> anyhow::Result<u32> {
+                let memory = memory_get(&mut caller)?;
+                let path = read_option_string(
+                    &memory,
+                    caller.as_context_mut(),
+                    path_is_some,
+                    path_ptr,
+                    path_len,
+                )?;
+                let authority = read_option_string(
+                    &memory,
+                    caller.as_context_mut(),
+                    authority_is_some,
+                    authority_ptr,
+                    authority_len,
+                )?;
+
+                let mut s = Some(Scheme::Https);
+                if scheme_is_some == 1 {
+                    s = Some(match scheme {
+                        0 => Scheme::Http,
+                        1 => Scheme::Https,
+                        _ => {
+                            let value = string_from_memory(
+                                &memory,
+                                caller.as_context_mut(),
+                                scheme_ptr.try_into()?,
+                                scheme_len.try_into()?,
+                            )?;
+                            Scheme::Other(value)
+                        }
+                    });
+                }
+                let m = match method {
+                    0 => Method::Get,
+                    1 => Method::Head,
+                    2 => Method::Post,
+                    3 => Method::Put,
+                    4 => Method::Delete,
+                    5 => Method::Connect,
+                    6 => Method::Options,
+                    7 => Method::Trace,
+                    8 => Method::Patch,
+                    _ => {
+                        let value = string_from_memory(
+                            &memory,
+                            caller.as_context_mut(),
+                            method_ptr.try_into()?,
+                            method_len.try_into()?,
+                        )?;
+                        Method::Other(value)
+                    }
+                };
+
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-outgoing-request'] call method={:?} path={:?} scheme={:?} authority={:?} headers={:?}",
+                    m,
+                    path,
+                    s,
+                    authority,
+                    headers
+                );
+                let result =
+                    Host::new_outgoing_request(ctx, m, path, s, authority, headers);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-outgoing-request'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "incoming-response-status",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<u32> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-status'] call id={:?}",
+                    id
+                );
+                let result = Ok(u32::from(Host::incoming_response_status(ctx, id)?));
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-status'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "drop-future-incoming-response",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-future-incoming-response'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_future_incoming_response(ctx, id);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-future-incoming-response'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "future-incoming-response-get",
+            move |mut caller: Caller<'_, T>, future: u32, ptr: i32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+
+                tracing::trace!(
+                    "[module='wasi:http/types' function='future-incoming-response-get'] call future={:?}",
+                    future
+                );
+                let result = Host::future_incoming_response_get(ctx, future);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='future-incoming-response-get'] return result={:?}",
+                    result
+                );
+                let response = result?;
+
+                let memory = memory_get(&mut caller)?;
+
+                // First == is_some
+                // Second == is_err
+                // Third == {ok: is_err = false, tag: is_err = true}
+                // Fourth == string ptr
+                // Fifth == string len
+                let result: [u32; 5] = match response {
+                    Some(inner) => match inner {
+                        Ok(value) => [1, 0, value, 0, 0],
+                        Err(error) => {
+                            let (tag, err_string) = match error {
+                                Error::InvalidUrl(e) => (0u32, e),
+                                Error::TimeoutError(e) => (1u32, e),
+                                Error::ProtocolError(e) => (2u32, e),
+                                Error::UnexpectedError(e) => (3u32, e),
+                            };
+                            let bytes = err_string.as_bytes();
+                            let len = bytes.len().try_into().unwrap();
+                            let ptr = allocate_guest_pointer(&mut caller, len)?;
+                            memory.write(caller.as_context_mut(), ptr as _, bytes)?;
+                            [1, 1, tag, ptr, len]
+                        }
+                    },
+                    None => [0, 0, 0, 0, 0],
+                };
+                let raw = u32_array_to_u8(&result);
+
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "listen-to-future-incoming-response",
+            move |mut caller: Caller<'_, T>, future: u32| -> anyhow::Result<u32> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='listen-to-future-incoming-response'] call future={:?}",
+                    future
+                );
+                let result = Host::listen_to_future_incoming_response(ctx, future);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='listen-to-future-incoming-response'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "incoming-response-consume",
+            move |mut caller: Caller<'_, T>, response: u32, ptr: i32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-consume'] call response={:?}",
+                    response
+                );
+                let result = Host::incoming_response_consume(ctx, response);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-consume'] return result={:?}",
+                    result
+                );
+                let stream = result?.unwrap_or(0);
+
+                let memory = memory_get(&mut caller).unwrap();
+
+                // First == is_some
+                // Second == stream_id
+                let result: [u32; 2] = [0, stream];
+                let raw = u32_array_to_u8(&result);
+
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:poll/poll",
+            "drop-pollable",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='drop-pollable'] call id={:?}",
+                    id
+                );
+                let result = poll::poll::Host::drop_pollable(ctx, id);
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='drop-pollable'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:poll/poll",
+            "poll-oneoff",
+            move |mut caller: Caller<'_, T>,
+                  base_ptr: u32,
+                  len: u32,
+                  out_ptr: u32|
+                  -> anyhow::Result<()> {
+                let memory = memory_get(&mut caller)?;
+
+                let mut vec = Vec::new();
+                let mut i = 0;
+                while i < len {
+                    let ptr = base_ptr + i * 4;
+                    let pollable_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr)?;
+                    vec.push(pollable_ptr);
+                    i = i + 1;
+                }
+
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='poll-oneoff'] call in={:?}",
+                    vec
+                );
+                let result = poll::poll::Host::poll_oneoff(ctx, vec);
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='poll-oneoff'] return result={:?}",
+                    result
+                );
+                let result = result?;
+
+                let result_len = result.len();
+                let result_ptr = allocate_guest_pointer(&mut caller, (4 * result_len).try_into()?)?;
+                let mut ptr = result_ptr;
+                for item in result.iter() {
+                    let completion: u32 = match item {
+                        true => 1,
+                        false => 0,
+                    };
+                    memory.write(caller.as_context_mut(), ptr as _, &completion.to_be_bytes())?;
+
+                    ptr = ptr + 4;
+                }
+
+                let result: [u32; 2] = [result_ptr, result_len.try_into()?];
+                let raw = u32_array_to_u8(&result);
+                memory.write(caller.as_context_mut(), out_ptr as _, &raw)?;
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "drop-input-stream",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-input-stream'] call id={:?}",
+                    id
+                );
+                let result = io::streams::Host::drop_input_stream(ctx, id);
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-input-stream'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "drop-output-stream",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-output-stream'] call id={:?}",
+                    id
+                );
+                let result = io::streams::Host::drop_output_stream(ctx, id);
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='drop-output-stream'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "read",
+            move |mut caller: Caller<'_, T>,
+                  stream: u32,
+                  len: u64,
+                  ptr: u32|
+                  -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='read'] call this={:?} len={:?}",
+                    stream,
+                    len
+                );
+                let result = io::streams::Host::read(ctx, stream, len);
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='read'] return result={:?}",
+                    result
+                );
+                let (bytes, status) = result?.map_err(|_| anyhow!("read failed"))?;
+
+                let done = match status {
+                    io::streams::StreamStatus::Open => 0,
+                    io::streams::StreamStatus::Ended => 1,
+                };
+                let body_len: u32 = bytes.len().try_into()?;
+                let out_ptr = allocate_guest_pointer(&mut caller, body_len)?;
+
+                // First == is_err
+                // Second == {ok: is_err = false, tag: is_err = true}
+                // Third == bytes length
+                // Fourth == enum status
+                let result: [u32; 4] = [0, out_ptr, body_len, done];
+                let raw = u32_array_to_u8(&result);
+
+                let memory = memory_get(&mut caller)?;
+                memory.write(caller.as_context_mut(), out_ptr as _, &bytes)?;
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "blocking-read",
+            move |mut caller: Caller<'_, T>,
+                  stream: u32,
+                  len: u64,
+                  ptr: u32|
+                  -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-read'] call this={:?} len={:?}",
+                    stream,
+                    len
+                );
+                let result = io::streams::Host::blocking_read(ctx, stream, len);
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-read'] return result={:?}",
+                    result
+                );
+                let (bytes, status) = result?.map_err(|_| anyhow!("read failed"))?;
+
+                let done = match status {
+                    io::streams::StreamStatus::Open => 0,
+                    io::streams::StreamStatus::Ended => 1,
+                };
+                let body_len: u32 = bytes.len().try_into()?;
+                let out_ptr = allocate_guest_pointer(&mut caller, body_len)?;
+
+                // First == is_err
+                // Second == {ok: is_err = false, tag: is_err = true}
+                // Third == bytes length
+                // Fourth == enum status
+                let result: [u32; 4] = [0, out_ptr, body_len, done];
+                let raw = u32_array_to_u8(&result);
+
+                let memory = memory_get(&mut caller)?;
+                memory.write(caller.as_context_mut(), out_ptr as _, &bytes)?;
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "subscribe-to-input-stream",
+            move |mut caller: Caller<'_, T>, stream: u32| -> anyhow::Result<u32> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-input-stream'] call stream={:?}",
+                    stream
+                );
+                let result = io::streams::Host::subscribe_to_input_stream(ctx, stream)?;
+                // TODO: necessary until this PR has been merged:
+                // https://github.com/bytecodealliance/wasmtime/pull/6877
+                let oneoff_result = poll::poll::Host::poll_oneoff(ctx, vec![result])?;
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='poll-oneoff'] return result={:?}",
+                    oneoff_result
+                );
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-input-stream'] return result=Ok({:?})",
+                    result
+                );
+                Ok(result)
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "subscribe-to-output-stream",
+            move |mut caller: Caller<'_, T>, stream: u32| -> anyhow::Result<u32> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-output-stream'] call stream={:?}",
+                    stream
+                );
+                let result = io::streams::Host::subscribe_to_output_stream(ctx, stream)?;
+                // TODO: necessary until this PR has been merged:
+                // https://github.com/bytecodealliance/wasmtime/pull/6877
+                let oneoff_result = poll::poll::Host::poll_oneoff(ctx, vec![result])?;
+                tracing::trace!(
+                    "[module='wasi:poll/poll' function='poll-oneoff'] return result={:?}",
+                    oneoff_result
+                );
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='subscribe-to-output-stream'] return result=Ok({:?})",
+                    result
+                );
+                Ok(result)
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "write",
+            move |mut caller: Caller<'_, T>,
+                  stream: u32,
+                  body_ptr: u32,
+                  body_len: u32,
+                  ptr: u32|
+                  -> anyhow::Result<()> {
+                let memory = memory_get(&mut caller)?;
+                let body =
+                    string_from_memory(&memory, caller.as_context_mut(), body_ptr, body_len)?;
+
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='write'] call stream={:?} body={:?}",
+                    stream,
+                    body
+                );
+                let result = io::streams::Host::write(ctx, stream, body.into());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='write'] return result={:?}",
+                    result
+                );
+                let (len, status) = result?.map_err(|_| anyhow!("write failed"))?;
+
+                let written: u32 = len.try_into()?;
+                let done: u32 = match status {
+                    io::streams::StreamStatus::Open => 0,
+                    io::streams::StreamStatus::Ended => 1,
+                };
+
+                // First == is_err
+                // Second == {ok: is_err = false, tag: is_err = true}
+                // Third == amount of bytes written
+                // Fifth == enum status
+                let result: [u32; 5] = [0, 0, written, 0, done];
+                let raw = u32_array_to_u8(&result);
+
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:io/streams",
+            "blocking-write",
+            move |mut caller: Caller<'_, T>,
+                  stream: u32,
+                  body_ptr: u32,
+                  body_len: u32,
+                  ptr: u32|
+                  -> anyhow::Result<()> {
+                let memory = memory_get(&mut caller)?;
+                let body =
+                    string_from_memory(&memory, caller.as_context_mut(), body_ptr, body_len)?;
+
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-write'] call stream={:?} body={:?}",
+                    stream,
+                    body
+                );
+                let result = io::streams::Host::blocking_write(ctx, stream, body.into());
+                tracing::trace!(
+                    "[module='wasi:io/streams' function='blocking-write'] return result={:?}",
+                    result
+                );
+                let (len, status) = result?.map_err(|_| anyhow!("write failed"))?;
+
+                let written: u32 = len.try_into()?;
+                let done: u32 = match status {
+                    io::streams::StreamStatus::Open => 0,
+                    io::streams::StreamStatus::Ended => 1,
+                };
+
+                // First == is_err
+                // Second == {ok: is_err = false, tag: is_err = true}
+                // Third == amount of bytes written
+                // Fifth == enum status
+                let result: [u32; 5] = [0, 0, written, 0, done];
+                let raw = u32_array_to_u8(&result);
+
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "drop-fields",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-fields'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_fields(ctx, id);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-fields'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "outgoing-request-write",
+            move |mut caller: Caller<'_, T>, request: u32, ptr: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='outgoing-request-write'] call request={:?}",
+                    request
+                );
+                let result = Host::outgoing_request_write(ctx, request);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='outgoing-request-write'] return result={:?}",
+                    result
+                );
+                let stream = result?
+                    .map_err(|_| anyhow!("no outgoing stream present"))?;
+
+                let memory = memory_get(&mut caller)?;
+                // First == is_some
+                // Second == stream_id
+                let result: [u32; 2] = [0, stream];
+                let raw = u32_array_to_u8(&result);
+
+                memory.write(caller.as_context_mut(), ptr as _, &raw)?;
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "drop-outgoing-request",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-outgoing-request'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_outgoing_request(ctx, id);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-outgoing-request'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "drop-incoming-response",
+            move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-incoming-response'] call id={:?}",
+                    id
+                );
+                let result = Host::drop_incoming_response(ctx, id);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='drop-incoming-response'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "new-fields",
+            move |mut caller: Caller<'_, T>, base_ptr: u32, len: u32| -> anyhow::Result<u32> {
+                let memory = memory_get(&mut caller)?;
+
+                let mut vec = Vec::new();
+                let mut i = 0;
+                // TODO: read this more efficiently as a single block.
+                while i < len {
+                    let ptr = base_ptr + i * 16;
+                    let name_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr)?;
+                    let name_len = u32_from_memory(&memory, caller.as_context_mut(), ptr + 4)?;
+                    let value_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr + 8)?;
+                    let value_len = u32_from_memory(&memory, caller.as_context_mut(), ptr + 12)?;
+
+                    let name =
+                        string_from_memory(&memory, caller.as_context_mut(), name_ptr, name_len)?;
+                    let value =
+                        string_from_memory(&memory, caller.as_context_mut(), value_ptr, value_len)?;
+
+                    vec.push((name, value));
+                    i = i + 1;
+                }
+
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-fields'] call entries={:?}",
+                    vec
+                );
+                let result = Host::new_fields(ctx, vec);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='new-fields'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "fields-entries",
+            move |mut caller: Caller<'_, T>, fields: u32, out_ptr: u32| -> anyhow::Result<()> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='fields-entries'] call fields={:?}",
+                    fields
+                );
+                let result = Host::fields_entries(ctx, fields);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='fields-entries'] return result={:?}",
+                    result
+                );
+                let entries = result?;
+
+                let header_len = entries.len();
+                let tuple_ptr = allocate_guest_pointer(&mut caller, (16 * header_len).try_into()?)?;
+                let mut ptr = tuple_ptr;
+                for item in entries.iter() {
+                    let name = &item.0;
+                    let value = &item.1;
+                    let name_len: u32 = name.len().try_into()?;
+                    let value_len: u32 = value.len().try_into()?;
+
+                    let name_ptr = allocate_guest_pointer(&mut caller, name_len)?;
+                    let value_ptr = allocate_guest_pointer(&mut caller, value_len)?;
+
+                    let memory = memory_get(&mut caller)?;
+                    memory.write(caller.as_context_mut(), name_ptr as _, &name.as_bytes())?;
+                    memory.write(caller.as_context_mut(), value_ptr as _, value)?;
+
+                    let pair: [u32; 4] = [name_ptr, name_len, value_ptr, value_len];
+                    let raw_pair = u32_array_to_u8(&pair);
+                    memory.write(caller.as_context_mut(), ptr as _, &raw_pair)?;
+
+                    ptr = ptr + 16;
+                }
+
+                let memory = memory_get(&mut caller)?;
+                let result: [u32; 2] = [tuple_ptr, header_len.try_into()?];
+                let raw = u32_array_to_u8(&result);
+                memory.write(caller.as_context_mut(), out_ptr as _, &raw)?;
+                Ok(())
+            },
+        )?;
+        linker.func_wrap(
+            "wasi:http/types",
+            "incoming-response-headers",
+            move |mut caller: Caller<'_, T>, handle: u32| -> anyhow::Result<u32> {
+                let ctx = get_cx(caller.data_mut());
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-headers'] call handle={:?}",
+                    handle
+                );
+                let result = Host::incoming_response_headers(ctx, handle);
+                tracing::trace!(
+                    "[module='wasi:http/types' function='incoming-response-headers'] return result={:?}",
+                    result
+                );
+                result
+            },
+        )?;
+        Ok(())
+    }
+}
diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/http_impl.rs
index 471ef164f..cd0cdfdf4 100644
--- a/crates/wasi-http/src/http_impl.rs
+++ b/crates/wasi-http/src/http_impl.rs
@@ -1,6 +1,8 @@
+use crate::bindings::http::types::{
+    FutureIncomingResponse, OutgoingRequest, RequestOptions, Scheme,
+};
 use crate::types::{ActiveFields, ActiveFuture, ActiveResponse, HttpResponse, TableHttpExt};
-use crate::wasi::http::types::{FutureIncomingResponse, OutgoingRequest, RequestOptions, Scheme};
-pub use crate::{WasiHttpCtx, WasiHttpView};
+use crate::WasiHttpView;
 use anyhow::Context;
 use bytes::{Bytes, BytesMut};
 use http_body_util::{BodyExt, Empty, Full};
@@ -15,7 +17,7 @@ use tokio_rustls::rustls::{self, OwnedTrustAnchor};
 use wasmtime_wasi::preview2::{StreamState, TableStreamExt};
 
 #[async_trait::async_trait]
-impl<T: WasiHttpView> crate::wasi::http::outgoing_handler::Host for T {
+impl<T: WasiHttpView> crate::bindings::http::outgoing_handler::Host for T {
     async fn handle(
         &mut self,
         request_id: OutgoingRequest,
@@ -30,6 +32,40 @@ impl<T: WasiHttpView> crate::wasi::http::outgoing_handler::Host for T {
     }
 }
 
+#[cfg(feature = "sync")]
+pub mod sync {
+    use crate::bindings::http::outgoing_handler::{
+        Host as AsyncHost, RequestOptions as AsyncRequestOptions,
+    };
+    use crate::bindings::sync::http::types::{
+        FutureIncomingResponse, OutgoingRequest, RequestOptions,
+    };
+    use crate::WasiHttpView;
+    use wasmtime_wasi::preview2::in_tokio;
+
+    // same boilerplate everywhere, converting between two identical types with different
+    // definition sites. one day wasmtime-wit-bindgen will make all this unnecessary
+    impl From<RequestOptions> for AsyncRequestOptions {
+        fn from(other: RequestOptions) -> Self {
+            Self {
+                connect_timeout_ms: other.connect_timeout_ms,
+                first_byte_timeout_ms: other.first_byte_timeout_ms,
+                between_bytes_timeout_ms: other.between_bytes_timeout_ms,
+            }
+        }
+    }
+
+    impl<T: WasiHttpView> crate::bindings::sync::http::outgoing_handler::Host for T {
+        fn handle(
+            &mut self,
+            request_id: OutgoingRequest,
+            options: Option<RequestOptions>,
+        ) -> wasmtime::Result<FutureIncomingResponse> {
+            in_tokio(async { AsyncHost::handle(self, request_id, options.map(|v| v.into())).await })
+        }
+    }
+}
+
 fn port_for_scheme(scheme: &Option<Scheme>) -> &str {
     match scheme {
         Some(s) => match s {
@@ -48,7 +84,7 @@ pub trait WasiHttpViewExt {
         &mut self,
         request_id: OutgoingRequest,
         options: Option<RequestOptions>,
-    ) -> wasmtime::Result<FutureIncomingResponse, crate::wasi::http::types::Error>;
+    ) -> wasmtime::Result<FutureIncomingResponse, crate::bindings::http::types::Error>;
 }
 
 #[async_trait::async_trait]
@@ -57,7 +93,8 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
         &mut self,
         request_id: OutgoingRequest,
         options: Option<RequestOptions>,
-    ) -> wasmtime::Result<FutureIncomingResponse, crate::wasi::http::types::Error> {
+    ) -> wasmtime::Result<FutureIncomingResponse, crate::bindings::http::types::Error> {
+        tracing::debug!("preparing outgoing request");
         let opts = options.unwrap_or(
             // TODO: Configurable defaults here?
             RequestOptions {
@@ -78,19 +115,20 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
             .get_request(request_id)
             .context("[handle_async] getting request")?
             .clone();
+        tracing::debug!("http request retrieved from table");
 
         let method = match request.method() {
-            crate::wasi::http::types::Method::Get => Method::GET,
-            crate::wasi::http::types::Method::Head => Method::HEAD,
-            crate::wasi::http::types::Method::Post => Method::POST,
-            crate::wasi::http::types::Method::Put => Method::PUT,
-            crate::wasi::http::types::Method::Delete => Method::DELETE,
-            crate::wasi::http::types::Method::Connect => Method::CONNECT,
-            crate::wasi::http::types::Method::Options => Method::OPTIONS,
-            crate::wasi::http::types::Method::Trace => Method::TRACE,
-            crate::wasi::http::types::Method::Patch => Method::PATCH,
-            crate::wasi::http::types::Method::Other(s) => {
-                return Err(crate::wasi::http::types::Error::InvalidUrl(format!(
+            crate::bindings::http::types::Method::Get => Method::GET,
+            crate::bindings::http::types::Method::Head => Method::HEAD,
+            crate::bindings::http::types::Method::Post => Method::POST,
+            crate::bindings::http::types::Method::Put => Method::PUT,
+            crate::bindings::http::types::Method::Delete => Method::DELETE,
+            crate::bindings::http::types::Method::Connect => Method::CONNECT,
+            crate::bindings::http::types::Method::Options => Method::OPTIONS,
+            crate::bindings::http::types::Method::Trace => Method::TRACE,
+            crate::bindings::http::types::Method::Patch => Method::PATCH,
+            crate::bindings::http::types::Method::Other(s) => {
+                return Err(crate::bindings::http::types::Error::InvalidUrl(format!(
                     "unknown method {}",
                     s
                 ))
@@ -102,7 +140,7 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
             Scheme::Http => "http://",
             Scheme::Https => "https://",
             Scheme::Other(s) => {
-                return Err(crate::wasi::http::types::Error::InvalidUrl(format!(
+                return Err(crate::bindings::http::types::Error::InvalidUrl(format!(
                     "unsupported scheme {}",
                     s
                 ))
@@ -117,6 +155,7 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
         };
         let tcp_stream = TcpStream::connect(authority.clone()).await?;
         let mut sender = if scheme == "https://" {
+            tracing::debug!("initiating client connection client with TLS");
             #[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))]
             {
                 //TODO: uncomment this code and make the tls implementation a feature decision.
@@ -144,10 +183,9 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
                 let mut parts = authority.split(":");
                 let host = parts.next().unwrap_or(&authority);
                 let domain = rustls::ServerName::try_from(host)?;
-                let stream = connector
-                    .connect(domain, tcp_stream)
-                    .await
-                    .map_err(|e| crate::wasi::http::types::Error::ProtocolError(e.to_string()))?;
+                let stream = connector.connect(domain, tcp_stream).await.map_err(|e| {
+                    crate::bindings::http::types::Error::ProtocolError(e.to_string())
+                })?;
 
                 let t = timeout(
                     connect_timeout,
@@ -157,16 +195,17 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
                 let (s, conn) = t?;
                 tokio::task::spawn(async move {
                     if let Err(err) = conn.await {
-                        println!("Connection failed: {:?}", err);
+                        tracing::debug!("[host/client] Connection failed: {:?}", err);
                     }
                 });
                 s
             }
             #[cfg(any(target_arch = "riscv64", target_arch = "s390x"))]
-            return Err(crate::wasi::http::types::Error::UnexpectedError(
+            return Err(crate::bindings::http::types::Error::UnexpectedError(
                 "unsupported architecture for SSL".to_string(),
             ));
         } else {
+            tracing::debug!("initiating client connection without TLS");
             let t = timeout(
                 connect_timeout,
                 hyper::client::conn::http1::handshake(tcp_stream),
@@ -175,7 +214,7 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
             let (s, conn) = t?;
             tokio::task::spawn(async move {
                 if let Err(err) = conn.await {
-                    println!("Connection failed: {:?}", err);
+                    tracing::debug!("[host/client] Connection failed: {:?}", err);
                 }
             });
             s
@@ -183,6 +222,7 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
 
         let url = scheme.to_owned() + &request.authority() + &request.path_with_query();
 
+        tracing::debug!("request to url {:?}", &url);
         let mut call = Request::builder()
             .method(method)
             .uri(url)
@@ -226,8 +266,11 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
             }
             None => Empty::<Bytes>::new().boxed(),
         };
-        let t = timeout(first_bytes_timeout, sender.send_request(call.body(body)?)).await?;
+        let request = call.body(body)?;
+        tracing::trace!("hyper request {:?}", request);
+        let t = timeout(first_bytes_timeout, sender.send_request(request)).await?;
         let mut res = t?;
+        tracing::trace!("hyper response {:?}", res);
         response.status = res.status().as_u16();
 
         let mut map = ActiveFields::new();
@@ -245,10 +288,13 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
         let mut buf: Vec<u8> = Vec::new();
         while let Some(next) = timeout(between_bytes_timeout, res.frame()).await? {
             let frame = next?;
+            tracing::debug!("response body next frame");
             if let Some(chunk) = frame.data_ref() {
+                tracing::trace!("response body chunk size {:?}", chunk.len());
                 buf.extend_from_slice(chunk);
             }
             if let Some(trailers) = frame.trailers_ref() {
+                tracing::debug!("response trailers present");
                 let mut map = ActiveFields::new();
                 for (name, value) in trailers.iter() {
                     let key = name.to_string();
@@ -266,6 +312,7 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
                     .push_fields(Box::new(map))
                     .context("[handle_async] pushing response trailers")?;
                 response.set_trailers(trailers);
+                tracing::debug!("http trailers saved to table");
             }
         }
 
@@ -273,6 +320,7 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
             .table_mut()
             .push_response(Box::new(response))
             .context("[handle_async] pushing response")?;
+        tracing::trace!("response body {:?}", std::str::from_utf8(&buf[..]).unwrap());
         let (stream_id, stream) = self
             .table_mut()
             .push_stream(Bytes::from(buf), response_id)
@@ -282,6 +330,7 @@ impl<T: WasiHttpView> WasiHttpViewExt for T {
             .get_response_mut(response_id)
             .context("[handle_async] getting mutable response")?;
         response.set_body(stream_id);
+        tracing::debug!("http response saved to table with id {:?}", response_id);
 
         self.http_ctx_mut().streams.insert(stream_id, stream);
 
diff --git a/crates/wasi-http/src/incoming_handler.rs b/crates/wasi-http/src/incoming_handler.rs
new file mode 100644
index 000000000..e65a88e27
--- /dev/null
+++ b/crates/wasi-http/src/incoming_handler.rs
@@ -0,0 +1,31 @@
+use crate::bindings::http::types::{IncomingRequest, ResponseOutparam};
+use crate::WasiHttpView;
+
+#[async_trait::async_trait]
+impl<T: WasiHttpView> crate::bindings::http::incoming_handler::Host for T {
+    async fn handle(
+        &mut self,
+        _request: IncomingRequest,
+        _response_out: ResponseOutparam,
+    ) -> wasmtime::Result<()> {
+        anyhow::bail!("unimplemented: [incoming_handler] handle")
+    }
+}
+
+#[cfg(feature = "sync")]
+pub mod sync {
+    use crate::bindings::http::incoming_handler::Host as AsyncHost;
+    use crate::bindings::sync::http::types::{IncomingRequest, ResponseOutparam};
+    use crate::WasiHttpView;
+    use wasmtime_wasi::preview2::in_tokio;
+
+    impl<T: WasiHttpView> crate::bindings::sync::http::incoming_handler::Host for T {
+        fn handle(
+            &mut self,
+            request: IncomingRequest,
+            response_out: ResponseOutparam,
+        ) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::handle(self, request, response_out).await })
+        }
+    }
+}
diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs
index c2fedf5eb..7a99a0b79 100644
--- a/crates/wasi-http/src/lib.rs
+++ b/crates/wasi-http/src/lib.rs
@@ -1,81 +1,102 @@
-use crate::component_impl::add_component_to_linker;
 pub use crate::http_impl::WasiHttpViewExt;
 pub use crate::types::{WasiHttpCtx, WasiHttpView};
 use core::fmt::Formatter;
 use std::fmt::{self, Display};
 
-wasmtime::component::bindgen!({
-    world: "wasi:http/proxy",
-    with: {
-        "wasi:io/streams": wasmtime_wasi::preview2::bindings::io::streams,
-        "wasi:poll/poll": wasmtime_wasi::preview2::bindings::poll::poll,
-    },
-    async: true,
-});
-
 pub mod component_impl;
 pub mod http_impl;
+pub mod incoming_handler;
+pub mod proxy;
 pub mod types;
 pub mod types_impl;
 
-pub fn add_to_component_linker<T>(linker: &mut wasmtime::component::Linker<T>) -> anyhow::Result<()>
-where
-    T: WasiHttpView
-        + WasiHttpViewExt
-        + crate::wasi::http::outgoing_handler::Host
-        + crate::wasi::http::types::Host,
-{
-    crate::wasi::http::outgoing_handler::add_to_linker(linker, |t| t)?;
-    crate::wasi::http::types::add_to_linker(linker, |t| t)?;
-    Ok(())
+pub mod bindings {
+    #[cfg(feature = "sync")]
+    pub mod sync {
+        pub(crate) mod _internal {
+            wasmtime::component::bindgen!({
+                path: "wit",
+                interfaces: "
+                    import wasi:http/incoming-handler
+                    import wasi:http/outgoing-handler
+                    import wasi:http/types
+                ",
+                tracing: true,
+                with: {
+                    "wasi:io/streams": wasmtime_wasi::preview2::bindings::sync_io::io::streams,
+                    "wasi:poll/poll": wasmtime_wasi::preview2::bindings::sync_io::poll::poll,
+                }
+            });
+        }
+        pub use self::_internal::wasi::http;
+    }
+
+    pub(crate) mod _internal_rest {
+        wasmtime::component::bindgen!({
+            path: "wit",
+            interfaces: "
+                import wasi:http/incoming-handler
+                import wasi:http/outgoing-handler
+                import wasi:http/types
+            ",
+            tracing: true,
+            async: true,
+            with: {
+                "wasi:io/streams": wasmtime_wasi::preview2::bindings::io::streams,
+                "wasi:poll/poll": wasmtime_wasi::preview2::bindings::poll::poll,
+            }
+        });
+    }
+
+    pub use self::_internal_rest::wasi::http;
+}
+
+pub fn add_to_linker<T: WasiHttpView>(linker: &mut wasmtime::Linker<T>) -> anyhow::Result<()> {
+    crate::component_impl::add_component_to_linker::<T>(linker, |t| t)
 }
 
-pub fn add_to_linker<T>(linker: &mut wasmtime::Linker<T>) -> anyhow::Result<()>
-where
-    T: WasiHttpView
-        + WasiHttpViewExt
-        + crate::wasi::http::outgoing_handler::Host
-        + crate::wasi::http::types::Host
-        + wasmtime_wasi::preview2::bindings::io::streams::Host
-        + wasmtime_wasi::preview2::bindings::poll::poll::Host,
-{
-    add_component_to_linker::<T>(linker, |t| t)
+pub mod sync {
+    use crate::types::WasiHttpView;
+
+    pub fn add_to_linker<T: WasiHttpView>(linker: &mut wasmtime::Linker<T>) -> anyhow::Result<()> {
+        crate::component_impl::sync::add_component_to_linker::<T>(linker, |t| t)
+    }
 }
 
-impl std::error::Error for crate::wasi::http::types::Error {}
+impl std::error::Error for crate::bindings::http::types::Error {}
 
-impl Display for crate::wasi::http::types::Error {
+impl Display for crate::bindings::http::types::Error {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         match self {
-            crate::wasi::http::types::Error::InvalidUrl(m) => {
+            crate::bindings::http::types::Error::InvalidUrl(m) => {
                 write!(f, "[InvalidUrl] {}", m)
             }
-            crate::wasi::http::types::Error::ProtocolError(m) => {
+            crate::bindings::http::types::Error::ProtocolError(m) => {
                 write!(f, "[ProtocolError] {}", m)
             }
-            crate::wasi::http::types::Error::TimeoutError(m) => {
+            crate::bindings::http::types::Error::TimeoutError(m) => {
                 write!(f, "[TimeoutError] {}", m)
             }
-            crate::wasi::http::types::Error::UnexpectedError(m) => {
+            crate::bindings::http::types::Error::UnexpectedError(m) => {
                 write!(f, "[UnexpectedError] {}", m)
             }
         }
     }
 }
 
-impl From<wasmtime_wasi::preview2::TableError> for crate::wasi::http::types::Error {
+impl From<wasmtime_wasi::preview2::TableError> for crate::bindings::http::types::Error {
     fn from(err: wasmtime_wasi::preview2::TableError) -> Self {
         Self::UnexpectedError(err.to_string())
     }
 }
 
-impl From<anyhow::Error> for crate::wasi::http::types::Error {
+impl From<anyhow::Error> for crate::bindings::http::types::Error {
     fn from(err: anyhow::Error) -> Self {
         Self::UnexpectedError(err.to_string())
     }
 }
 
-impl From<std::io::Error> for crate::wasi::http::types::Error {
+impl From<std::io::Error> for crate::bindings::http::types::Error {
     fn from(err: std::io::Error) -> Self {
         let message = err.to_string();
         match err.kind() {
@@ -92,13 +113,13 @@ impl From<std::io::Error> for crate::wasi::http::types::Error {
     }
 }
 
-impl From<http::Error> for crate::wasi::http::types::Error {
+impl From<http::Error> for crate::bindings::http::types::Error {
     fn from(err: http::Error) -> Self {
         Self::InvalidUrl(err.to_string())
     }
 }
 
-impl From<hyper::Error> for crate::wasi::http::types::Error {
+impl From<hyper::Error> for crate::bindings::http::types::Error {
     fn from(err: hyper::Error) -> Self {
         let message = err.message().to_string();
         if err.is_timeout() {
@@ -118,14 +139,14 @@ impl From<hyper::Error> for crate::wasi::http::types::Error {
     }
 }
 
-impl From<tokio::time::error::Elapsed> for crate::wasi::http::types::Error {
+impl From<tokio::time::error::Elapsed> for crate::bindings::http::types::Error {
     fn from(err: tokio::time::error::Elapsed) -> Self {
         Self::TimeoutError(err.to_string())
     }
 }
 
 #[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))]
-impl From<rustls::client::InvalidDnsNameError> for crate::wasi::http::types::Error {
+impl From<rustls::client::InvalidDnsNameError> for crate::bindings::http::types::Error {
     fn from(_err: rustls::client::InvalidDnsNameError) -> Self {
         Self::InvalidUrl("invalid dnsname".to_string())
     }
diff --git a/crates/wasi-http/src/proxy.rs b/crates/wasi-http/src/proxy.rs
new file mode 100644
index 000000000..8cd78d700
--- /dev/null
+++ b/crates/wasi-http/src/proxy.rs
@@ -0,0 +1,68 @@
+use crate::{bindings, WasiHttpView};
+use wasmtime_wasi::preview2;
+
+wasmtime::component::bindgen!({
+    world: "wasi:http/proxy",
+    tracing: true,
+    async: true,
+    with: {
+        "wasi:cli/stderr": preview2::bindings::cli::stderr,
+        "wasi:cli/stdin": preview2::bindings::cli::stdin,
+        "wasi:cli/stdout": preview2::bindings::cli::stdout,
+        "wasi:clocks/monotonic-clock": preview2::bindings::clocks::monotonic_clock,
+        "wasi:clocks/timezone": preview2::bindings::clocks::timezone,
+        "wasi:clocks/wall-clock": preview2::bindings::clocks::wall_clock,
+        "wasi:http/incoming-handler": bindings::http::incoming_handler,
+        "wasi:http/outgoing-handler": bindings::http::outgoing_handler,
+        "wasi:http/types": bindings::http::types,
+        "wasi:io/streams": preview2::bindings::io::streams,
+        "wasi:poll/poll": preview2::bindings::poll::poll,
+        "wasi:random/random": preview2::bindings::random::random,
+    },
+});
+
+pub fn add_to_linker<T>(l: &mut wasmtime::component::Linker<T>) -> anyhow::Result<()>
+where
+    T: WasiHttpView + bindings::http::types::Host,
+{
+    bindings::http::incoming_handler::add_to_linker(l, |t| t)?;
+    bindings::http::outgoing_handler::add_to_linker(l, |t| t)?;
+    bindings::http::types::add_to_linker(l, |t| t)?;
+    Ok(())
+}
+
+#[cfg(feature = "sync")]
+pub mod sync {
+    use crate::{bindings, WasiHttpView};
+    use wasmtime_wasi::preview2;
+
+    wasmtime::component::bindgen!({
+        world: "wasi:http/proxy",
+        tracing: true,
+        async: false,
+        with: {
+            "wasi:cli/stderr": preview2::bindings::cli::stderr,
+            "wasi:cli/stdin": preview2::bindings::cli::stdin,
+            "wasi:cli/stdout": preview2::bindings::cli::stdout,
+            "wasi:clocks/monotonic-clock": preview2::bindings::clocks::monotonic_clock,
+            "wasi:clocks/timezone": preview2::bindings::clocks::timezone,
+            "wasi:clocks/wall-clock": preview2::bindings::clocks::wall_clock,
+            "wasi:http/incoming-handler": bindings::sync::http::incoming_handler,
+            "wasi:http/outgoing-handler": bindings::sync::http::outgoing_handler,
+            "wasi:http/types": bindings::sync::http::types,
+            "wasi:io/streams": preview2::bindings::sync_io::io::streams,
+            "wasi:poll/poll": preview2::bindings::sync_io::poll::poll,
+            "wasi:random/random": preview2::bindings::random::random,
+        },
+    });
+
+    pub fn add_to_linker<T>(l: &mut wasmtime::component::Linker<T>) -> anyhow::Result<()>
+    where
+        T: WasiHttpView + bindings::sync::http::types::Host,
+    {
+        bindings::sync::http::incoming_handler::add_to_linker(l, |t| t)?;
+        bindings::sync::http::outgoing_handler::add_to_linker(l, |t| t)?;
+        bindings::sync::http::types::add_to_linker(l, |t| t)?;
+        Ok(())
+    }
+}
diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs
index 35503b7a6..e611bc4ac 100644
--- a/crates/wasi-http/src/types.rs
+++ b/crates/wasi-http/src/types.rs
@@ -1,7 +1,7 @@
 //! Implements the base structure (i.e. [WasiHttpCtx]) that will provide the
 //! implementation of the wasi-http API.
 
-use crate::wasi::http::types::{
+use crate::bindings::http::types::{
     IncomingStream, Method, OutgoingRequest, OutgoingStream, RequestOptions, Scheme,
 };
 use bytes::Bytes;
@@ -183,7 +183,7 @@ impl HttpResponse for ActiveResponse {
     }
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct ActiveFuture {
     request_id: OutgoingRequest,
     options: Option<RequestOptions>,
@@ -368,6 +368,7 @@ impl TableHttpExt for Table {
     }
 
     fn push_stream(&mut self, content: Bytes, parent: u32) -> Result<(u32, Stream), TableError> {
+        tracing::debug!("preparing http body stream");
         let (a, b) = tokio::io::duplex(MAX_BUF_SIZE);
         let (_, write_stream) = tokio::io::split(a);
         let (read_stream, _) = tokio::io::split(b);
@@ -388,6 +389,12 @@ impl TableHttpExt for Table {
         let stream = Stream::new(input_id, output_id, parent);
         let cloned_stream = stream.clone();
         let stream_id = self.push(Box::new(Box::new(stream)))?;
+        tracing::trace!(
+            "http body stream details ( id: {:?}, input: {:?}, output: {:?} )",
+            stream_id,
+            input_id,
+            output_id
+        );
         Ok((stream_id, cloned_stream))
     }
     fn get_stream(&self, id: u32) -> Result<&Stream, TableError> {
diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs
index 11d16a4ab..8cf3a7e3d 100644
--- a/crates/wasi-http/src/types_impl.rs
+++ b/crates/wasi-http/src/types_impl.rs
@@ -1,17 +1,17 @@
-use crate::http_impl::WasiHttpViewExt;
-use crate::types::{ActiveFields, ActiveRequest, HttpRequest, TableHttpExt};
-use crate::wasi::http::types::{
+use crate::bindings::http::types::{
     Error, Fields, FutureIncomingResponse, Headers, IncomingRequest, IncomingResponse,
     IncomingStream, Method, OutgoingRequest, OutgoingResponse, OutgoingStream, ResponseOutparam,
     Scheme, StatusCode, Trailers,
 };
+use crate::http_impl::WasiHttpViewExt;
+use crate::types::{ActiveFields, ActiveRequest, HttpRequest, TableHttpExt};
 use crate::WasiHttpView;
 use anyhow::{anyhow, bail, Context};
 use bytes::Bytes;
 use wasmtime_wasi::preview2::{bindings::poll::poll::Pollable, HostPollable, TablePollableExt};
 
 #[async_trait::async_trait]
-impl<T: WasiHttpView + WasiHttpViewExt> crate::wasi::http::types::Host for T {
+impl<T: WasiHttpView + WasiHttpViewExt> crate::bindings::http::types::Host for T {
     async fn drop_fields(&mut self, fields: Fields) -> wasmtime::Result<()> {
         self.table_mut()
             .delete_fields(fields)
@@ -359,8 +359,15 @@ impl<T: WasiHttpView + WasiHttpViewExt> crate::wasi::http::types::Host for T {
                         let response = self.handle_async(f.request_id(), f.options()).await;
                         match response {
                             Ok(id) => {
+                                tracing::debug!(
+                                    "including response id to future incoming response"
+                                );
                                 let future_mut = self.table_mut().get_future_mut(future)?;
                                 future_mut.set_response_id(id);
+                                tracing::trace!(
+                                    "future incoming response details {:?}",
+                                    *future_mut
+                                );
                             }
                             _ => {}
                         }
@@ -383,6 +390,7 @@ impl<T: WasiHttpView + WasiHttpViewExt> crate::wasi::http::types::Host for T {
         Ok(match f.pollable_id() {
             Some(pollable_id) => pollable_id,
             None => {
+                tracing::debug!("including pollable id to future incoming response");
                 let pollable =
                     HostPollable::Closure(Box::new(|| Box::pin(futures::future::ready(Ok(())))));
                 let pollable_id = self
@@ -394,8 +402,295 @@ impl<T: WasiHttpView + WasiHttpViewExt> crate::wasi::http::types::Host for T {
                     .get_future_mut(future)
                     .context("[listen_to_future_incoming_response] getting future")?;
                 f.set_pollable_id(pollable_id);
+                tracing::trace!("future incoming response details {:?}", *f);
                 pollable_id
             }
         })
     }
 }
+
+#[cfg(feature = "sync")]
+pub mod sync {
+    use crate::bindings::http::types::{
+        Error as AsyncError, Host as AsyncHost, Method as AsyncMethod, Scheme as AsyncScheme,
+    };
+    use crate::bindings::sync::http::types::{
+        Error, Fields, FutureIncomingResponse, Headers, IncomingRequest, IncomingResponse,
+        IncomingStream, Method, OutgoingRequest, OutgoingResponse, OutgoingStream,
+        ResponseOutparam, Scheme, StatusCode, Trailers,
+    };
+    use crate::http_impl::WasiHttpViewExt;
+    use crate::WasiHttpView;
+    use wasmtime_wasi::preview2::{bindings::poll::poll::Pollable, in_tokio};
+
+    // same boilerplate everywhere, converting between two identical types with different
+    // definition sites. one day wasmtime-wit-bindgen will make all this unnecessary
+    impl From<AsyncError> for Error {
+        fn from(other: AsyncError) -> Self {
+            match other {
+                AsyncError::InvalidUrl(v) => Self::InvalidUrl(v),
+                AsyncError::ProtocolError(v) => Self::ProtocolError(v),
+                AsyncError::TimeoutError(v) => Self::TimeoutError(v),
+                AsyncError::UnexpectedError(v) => Self::UnexpectedError(v),
+            }
+        }
+    }
+
+    impl From<Error> for AsyncError {
+        fn from(other: Error) -> Self {
+            match other {
+                Error::InvalidUrl(v) => Self::InvalidUrl(v),
+                Error::ProtocolError(v) => Self::ProtocolError(v),
+                Error::TimeoutError(v) => Self::TimeoutError(v),
+                Error::UnexpectedError(v) => Self::UnexpectedError(v),
+            }
+        }
+    }
+
+    impl From<AsyncMethod> for Method {
+        fn from(other: AsyncMethod) -> Self {
+            match other {
+                AsyncMethod::Connect => Self::Connect,
+                AsyncMethod::Delete => Self::Delete,
+                AsyncMethod::Get => Self::Get,
+                AsyncMethod::Head => Self::Head,
+                AsyncMethod::Options => Self::Options,
+                AsyncMethod::Patch => Self::Patch,
+                AsyncMethod::Post => Self::Post,
+                AsyncMethod::Put => Self::Put,
+                AsyncMethod::Trace => Self::Trace,
+                AsyncMethod::Other(v) => Self::Other(v),
+            }
+        }
+    }
+
+    impl From<Method> for AsyncMethod {
+        fn from(other: Method) -> Self {
+            match other {
+                Method::Connect => Self::Connect,
+                Method::Delete => Self::Delete,
+                Method::Get => Self::Get,
+                Method::Head => Self::Head,
+                Method::Options => Self::Options,
+                Method::Patch => Self::Patch,
+                Method::Post => Self::Post,
+                Method::Put => Self::Put,
+                Method::Trace => Self::Trace,
+                Method::Other(v) => Self::Other(v),
+            }
+        }
+    }
+
+    impl From<AsyncScheme> for Scheme {
+        fn from(other: AsyncScheme) -> Self {
+            match other {
+                AsyncScheme::Http => Self::Http,
+                AsyncScheme::Https => Self::Https,
+                AsyncScheme::Other(v) => Self::Other(v),
+            }
+        }
+    }
+
+    impl From<Scheme> for AsyncScheme {
+        fn from(other: Scheme) -> Self {
+            match other {
+                Scheme::Http => Self::Http,
+                Scheme::Https => Self::Https,
+                Scheme::Other(v) => Self::Other(v),
+            }
+        }
+    }
+
+    impl<T: WasiHttpView + WasiHttpViewExt> crate::bindings::sync::http::types::Host for T {
+        fn drop_fields(&mut self, fields: Fields) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::drop_fields(self, fields).await })
+        }
+        fn new_fields(&mut self, entries: Vec<(String, String)>) -> wasmtime::Result<Fields> {
+            in_tokio(async { AsyncHost::new_fields(self, entries).await })
+        }
+        fn fields_get(&mut self, fields: Fields, name: String) -> wasmtime::Result<Vec<Vec<u8>>> {
+            in_tokio(async { AsyncHost::fields_get(self, fields, name).await })
+        }
+        fn fields_set(
+            &mut self,
+            fields: Fields,
+            name: String,
+            value: Vec<Vec<u8>>,
+        ) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::fields_set(self, fields, name, value).await })
+        }
+        fn fields_delete(&mut self, fields: Fields, name: String) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::fields_delete(self, fields, name).await })
+        }
+        fn fields_append(
+            &mut self,
+            fields: Fields,
+            name: String,
+            value: Vec<u8>,
+        ) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::fields_append(self, fields, name, value).await })
+        }
+        fn fields_entries(&mut self, fields: Fields) -> wasmtime::Result<Vec<(String, Vec<u8>)>> {
+            in_tokio(async { AsyncHost::fields_entries(self, fields).await })
+        }
+        fn fields_clone(&mut self, fields: Fields) -> wasmtime::Result<Fields> {
+            in_tokio(async { AsyncHost::fields_clone(self, fields).await })
+        }
+        fn finish_incoming_stream(
+            &mut self,
+            stream_id: IncomingStream,
+        ) -> wasmtime::Result<Option<Trailers>> {
+            in_tokio(async { AsyncHost::finish_incoming_stream(self, stream_id).await })
+        }
+        fn finish_outgoing_stream(
+            &mut self,
+            stream: OutgoingStream,
+            trailers: Option<Trailers>,
+        ) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::finish_outgoing_stream(self, stream, trailers).await })
+        }
+        fn drop_incoming_request(&mut self, request: IncomingRequest) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::drop_incoming_request(self, request).await })
+        }
+        fn drop_outgoing_request(&mut self, request: OutgoingRequest) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::drop_outgoing_request(self, request).await })
+        }
+        fn incoming_request_method(
+            &mut self,
+            request: IncomingRequest,
+        ) -> wasmtime::Result<Method> {
+            in_tokio(async { AsyncHost::incoming_request_method(self, request).await })
+                .map(Method::from)
+        }
+        fn incoming_request_path_with_query(
+            &mut self,
+            request: IncomingRequest,
+        ) -> wasmtime::Result<Option<String>> {
+            in_tokio(async { AsyncHost::incoming_request_path_with_query(self, request).await })
+        }
+        fn incoming_request_scheme(
+            &mut self,
+            request: IncomingRequest,
+        ) -> wasmtime::Result<Option<Scheme>> {
+            Ok(
+                in_tokio(async { AsyncHost::incoming_request_scheme(self, request).await })?
+                    .map(Scheme::from),
+            )
+        }
+        fn incoming_request_authority(
+            &mut self,
+            request: IncomingRequest,
+        ) -> wasmtime::Result<Option<String>> {
+            in_tokio(async { AsyncHost::incoming_request_authority(self, request).await })
+        }
+        fn incoming_request_headers(
+            &mut self,
+            request: IncomingRequest,
+        ) -> wasmtime::Result<Headers> {
+            in_tokio(async { AsyncHost::incoming_request_headers(self, request).await })
+        }
+        fn incoming_request_consume(
+            &mut self,
+            request: IncomingRequest,
+        ) -> wasmtime::Result<Result<IncomingStream, ()>> {
+            in_tokio(async { AsyncHost::incoming_request_consume(self, request).await })
+        }
+        fn new_outgoing_request(
+            &mut self,
+            method: Method,
+            path_with_query: Option<String>,
+            scheme: Option<Scheme>,
+            authority: Option<String>,
+            headers: Headers,
+        ) -> wasmtime::Result<OutgoingRequest> {
+            in_tokio(async {
+                AsyncHost::new_outgoing_request(
+                    self,
+                    method.into(),
+                    path_with_query,
+                    scheme.map(AsyncScheme::from),
+                    authority,
+                    headers,
+                )
+                .await
+            })
+        }
+        fn outgoing_request_write(
+            &mut self,
+            request: OutgoingRequest,
+        ) -> wasmtime::Result<Result<OutgoingStream, ()>> {
+            in_tokio(async { AsyncHost::outgoing_request_write(self, request).await })
+        }
+        fn drop_response_outparam(&mut self, response: ResponseOutparam) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::drop_response_outparam(self, response).await })
+        }
+        fn set_response_outparam(
+            &mut self,
+            outparam: ResponseOutparam,
+            response: Result<OutgoingResponse, Error>,
+        ) -> wasmtime::Result<Result<(), ()>> {
+            in_tokio(async {
+                AsyncHost::set_response_outparam(self, outparam, response.map_err(AsyncError::from))
+                    .await
+            })
+        }
+        fn drop_incoming_response(&mut self, response: IncomingResponse) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::drop_incoming_response(self, response).await })
+        }
+        fn drop_outgoing_response(&mut self, response: OutgoingResponse) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::drop_outgoing_response(self, response).await })
+        }
+        fn incoming_response_status(
+            &mut self,
+            response: IncomingResponse,
+        ) -> wasmtime::Result<StatusCode> {
+            in_tokio(async { AsyncHost::incoming_response_status(self, response).await })
+        }
+        fn incoming_response_headers(
+            &mut self,
+            response: IncomingResponse,
+        ) -> wasmtime::Result<Headers> {
+            in_tokio(async { AsyncHost::incoming_response_headers(self, response).await })
+        }
+        fn incoming_response_consume(
+            &mut self,
+            response: IncomingResponse,
+        ) -> wasmtime::Result<Result<IncomingStream, ()>> {
+            in_tokio(async { AsyncHost::incoming_response_consume(self, response).await })
+        }
+        fn new_outgoing_response(
+            &mut self,
+            status_code: StatusCode,
+            headers: Headers,
+        ) -> wasmtime::Result<OutgoingResponse> {
+            in_tokio(async { AsyncHost::new_outgoing_response(self, status_code, headers).await })
+        }
+        fn outgoing_response_write(
+            &mut self,
+            response: OutgoingResponse,
+        ) -> wasmtime::Result<Result<OutgoingStream, ()>> {
+            in_tokio(async { AsyncHost::outgoing_response_write(self, response).await })
+        }
+        fn drop_future_incoming_response(
+            &mut self,
+            future: FutureIncomingResponse,
+        ) -> wasmtime::Result<()> {
+            in_tokio(async { AsyncHost::drop_future_incoming_response(self, future).await })
+        }
+        fn future_incoming_response_get(
+            &mut self,
+            future: FutureIncomingResponse,
+        ) -> wasmtime::Result<Option<Result<IncomingResponse, Error>>> {
+            Ok(
+                in_tokio(async { AsyncHost::future_incoming_response_get(self, future).await })?
+                    .map(|v| v.map_err(Error::from)),
+            )
+        }
+        fn listen_to_future_incoming_response(
+            &mut self,
+            future: FutureIncomingResponse,
+        ) -> wasmtime::Result<Pollable> {
+            in_tokio(async { AsyncHost::listen_to_future_incoming_response(self, future).await })
+        }
+    }
+}
diff --git a/crates/wasi-threads/Cargo.toml b/crates/wasi-threads/Cargo.toml
index 28408678b..b1f82c463 100644
--- a/crates/wasi-threads/Cargo.toml
+++ b/crates/wasi-threads/Cargo.toml
@@ -17,4 +17,6 @@ log = { workspace = true }
 rand = "0.8"
 wasi-common = { workspace = true }
 wasmtime = { workspace = true }
-wasmtime-wasi = { workspace = true, features = ["exit"] }
+wasmtime-wasi = { workspace = true, default-features = true, features = [
+    "exit",
+] }
diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs
index 170465ba3..96a738e17 100644
--- a/crates/wasi/src/preview2/mod.rs
+++ b/crates/wasi/src/preview2/mod.rs
@@ -167,7 +167,7 @@ where
     }
 }
 
-pub(crate) fn in_tokio<F: std::future::Future>(f: F) -> F::Output {
+pub fn in_tokio<F: std::future::Future>(f: F) -> F::Output {
     match tokio::runtime::Handle::try_current() {
         Ok(h) => {
             let _enter = h.enter();
diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml
index c559d0ded..6aa56e8f5 100644
--- a/crates/wasmtime/Cargo.toml
+++ b/crates/wasmtime/Cargo.toml
@@ -58,7 +58,7 @@ features = [
 
 [dev-dependencies]
 tempfile = "3.0"
-wasmtime-wasi = { path = "../wasi" }
+wasmtime-wasi = { path = "../wasi", default-features = true }
 wasi-cap-std-sync = { path = "../wasi-common/cap-std-sync" }
 
 [features]
diff --git a/src/commands/run.rs b/src/commands/run.rs
index a93eaecd0..968ad59d2 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -35,8 +35,8 @@ use wasmtime_wasi_nn::WasiNnCtx;
 #[cfg(feature = "wasi-threads")]
 use wasmtime_wasi_threads::WasiThreadsCtx;
 
-// #[cfg(feature = "wasi-http")]
-// use wasmtime_wasi_http::WasiHttpCtx;
+#[cfg(feature = "wasi-http")]
+use wasmtime_wasi_http::WasiHttpCtx;
 
 fn parse_module(s: OsString) -> anyhow::Result<PathBuf> {
     // Do not accept wasmtime subcommand names as the module name
@@ -667,12 +667,8 @@ impl RunCommand {
 
                 let component = module.unwrap_component();
 
-                let (command, _instance) = preview2::command::sync::Command::instantiate(
-                    &mut *store,
-                    &component,
-                    &linker,
-                )?;
-
+                let (command, _instance) =
+                    preview2::command::sync::Command::instantiate(&mut *store, component, linker)?;
                 let result = command
                     .wasi_cli_run()
                     .call_run(&mut *store)
@@ -912,7 +908,7 @@ impl RunCommand {
             match linker {
                 CliLinker::Core(linker) => {
                     if self.preview2 {
-                        wasmtime_wasi::preview2::preview1::add_to_linker_sync(linker)?;
+                        preview2::preview1::add_to_linker_sync(linker)?;
                         self.set_preview2_ctx(store)?;
                     } else {
                         wasmtime_wasi::add_to_linker(linker, |host| {
@@ -923,7 +919,7 @@ impl RunCommand {
                 }
                 #[cfg(feature = "component-model")]
                 CliLinker::Component(linker) => {
-                    wasmtime_wasi::preview2::command::sync::add_to_linker(linker)?;
+                    preview2::command::sync::add_to_linker(linker)?;
                     self.set_preview2_ctx(store)?;
                 }
             }
@@ -998,7 +994,16 @@ impl RunCommand {
             }
             #[cfg(feature = "wasi-http")]
             {
-                bail!("wasi-http support will be swapped over to component CLI support soon");
+                match linker {
+                    CliLinker::Core(linker) => {
+                        wasmtime_wasi_http::sync::add_to_linker(linker)?;
+                    }
+                    #[cfg(feature = "component-model")]
+                    CliLinker::Component(linker) => {
+                        wasmtime_wasi_http::proxy::sync::add_to_linker(linker)?;
+                    }
+                }
+                store.data_mut().wasi_http = Some(Arc::new(WasiHttpCtx::new()));
             }
         }
 
@@ -1096,8 +1101,8 @@ struct Host {
     wasi_nn: Option<Arc<WasiNnCtx>>,
     #[cfg(feature = "wasi-threads")]
     wasi_threads: Option<Arc<WasiThreadsCtx<Host>>>,
-    // #[cfg(feature = "wasi-http")]
-    // wasi_http: Option<WasiHttp>,
+    #[cfg(feature = "wasi-http")]
+    wasi_http: Option<Arc<WasiHttpCtx>>,
     limits: StoreLimits,
     guest_profiler: Option<Arc<GuestProfiler>>,
 }
@@ -1131,6 +1136,18 @@ impl preview2::preview1::WasiPreview1View for Host {
     }
 }
 
+#[cfg(feature = "wasi-http")]
+impl wasmtime_wasi_http::types::WasiHttpView for Host {
+    fn http_ctx(&self) -> &WasiHttpCtx {
+        self.wasi_http.as_ref().unwrap()
+    }
+
+    fn http_ctx_mut(&mut self) -> &mut WasiHttpCtx {
+        let ctx = self.wasi_http.as_mut().unwrap();
+        Arc::get_mut(ctx).expect("wasi-http is not compatible with threads")
+    }
+}
+
 #[cfg(not(unix))]
 fn ctx_set_listenfd(num_fd: usize, _builder: &mut WasiCtxBuilder) -> Result<usize> {
     Ok(num_fd)


Test patch:
diff --git a/crates/test-programs/Cargo.toml b/crates/test-programs/Cargo.toml
index b2f7ddd4f..f64082e77 100644
--- a/crates/test-programs/Cargo.toml
+++ b/crates/test-programs/Cargo.toml
@@ -21,6 +21,7 @@ http-body-util = "0.1.0-rc.2"
 hyper = { version = "1.0.0-rc.3", features = ["full"] }
 is-terminal = { workspace = true }
 tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros"] }
+tracing = { workspace = true }
 
 [dev-dependencies]
 anyhow = { workspace = true }
@@ -36,12 +37,14 @@ wasmtime = { workspace = true, features = ['cranelift', 'component-model'] }
 
 wasi-common = { workspace = true }
 wasi-cap-std-sync = { workspace = true }
-wasmtime-wasi = { workspace = true, features = ["tokio"] }
+wasmtime-wasi = { workspace = true, default-features = true, features = [
+    "tokio",
+] }
 cap-std = { workspace = true }
 cap-rand = { workspace = true }
 tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
 
-wasmtime-wasi-http = { workspace = true }
+wasmtime-wasi-http = { workspace = true, features = ["sync"] }
 
 [features]
 test_programs = []
diff --git a/crates/test-programs/src/http_server.rs b/crates/test-programs/src/http_server.rs
index 6d3e6231f..ff4c0d9cd 100644
--- a/crates/test-programs/src/http_server.rs
+++ b/crates/test-programs/src/http_server.rs
@@ -1,16 +1,23 @@
+use anyhow::Context;
 use http_body_util::{combinators::BoxBody, BodyExt, Full};
 use hyper::{body::Bytes, service::service_fn, Request, Response};
 use std::{
-    net::{SocketAddr, TcpListener, TcpStream},
-    sync::OnceLock,
+    future::Future,
+    net::{SocketAddr, TcpListener},
+    sync::{mpsc, OnceLock},
+    time::Duration,
 };
 
+const DEFAULT_TIMEOUT: Duration = Duration::from_secs(50);
+
 async fn test(
     mut req: Request<hyper::body::Incoming>,
 ) -> http::Result<Response<BoxBody<Bytes, std::convert::Infallible>>> {
+    tracing::debug!("preparing mocked response",);
     let method = req.method().to_string();
     let body = req.body_mut().collect().await.unwrap();
     let buf = body.to_bytes();
+    tracing::trace!("hyper request body size {:?}", buf.len());
 
     Response::builder()
         .status(http::StatusCode::OK)
@@ -19,12 +26,61 @@ async fn test(
         .body(Full::<Bytes>::from(buf).boxed())
 }
 
-async fn serve_http1_connection(stream: TcpStream) -> Result<(), hyper::Error> {
-    let mut builder = hyper::server::conn::http1::Builder::new();
-    let http = builder.keep_alive(false).pipeline_flush(true);
-    stream.set_nonblocking(true).unwrap();
-    let io = tokio::net::TcpStream::from_std(stream).unwrap();
-    http.serve_connection(io, service_fn(test)).await
+struct ServerHttp1 {
+    receiver: mpsc::Receiver<anyhow::Result<()>>,
+}
+
+impl ServerHttp1 {
+    fn new() -> Self {
+        tracing::debug!("initializing http1 server");
+        static CELL_HTTP1: OnceLock<TcpListener> = OnceLock::new();
+        let listener = CELL_HTTP1.get_or_init(|| {
+            let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
+            tracing::debug!("preparing tcp listener at localhost:3000");
+            TcpListener::bind(addr).unwrap()
+        });
+        let (sender, receiver) = mpsc::channel::<anyhow::Result<()>>();
+        std::thread::spawn(move || {
+            tracing::debug!("dedicated thread to start listening");
+            match tokio::runtime::Builder::new_current_thread()
+                .enable_all()
+                .build()
+            {
+                Ok(rt) => {
+                    tracing::debug!("using tokio runtime");
+                    sender
+                        .send(rt.block_on(async move {
+                            tracing::debug!("preparing to accept connection");
+                            let (stream, _) = listener.accept().map_err(anyhow::Error::from)?;
+                            tracing::trace!("tcp stream {:?}", stream);
+
+                            let mut builder = hyper::server::conn::http1::Builder::new();
+                            let http = builder.keep_alive(false).pipeline_flush(true);
+                            let io = tokio::net::TcpStream::from_std(stream)
+                                .map_err(anyhow::Error::from)?;
+
+                            tracing::debug!("preparing to bind connection to service");
+                            let conn = http.serve_connection(io, service_fn(test)).await;
+                            tracing::trace!("connection result {:?}", conn);
+                            conn.map_err(anyhow::Error::from)
+                        }))
+                        .expect("value sent from http1 server dedicated thread");
+                }
+                Err(e) => {
+                    tracing::debug!("unable to start tokio runtime");
+                    sender.send(Err(anyhow::Error::from(e))).unwrap()
+                }
+            };
+        });
+        Self { receiver }
+    }
+
+    fn shutdown(self) -> anyhow::Result<()> {
+        tracing::debug!("shutting down http1 server");
+        self.receiver
+            .recv_timeout(DEFAULT_TIMEOUT)
+            .context("value received from http1 server dedicated thread")?
+    }
 }
 
 #[derive(Clone)]
@@ -33,7 +89,7 @@ pub struct TokioExecutor;
 
 impl<F> hyper::rt::Executor<F> for TokioExecutor
 where
-    F: std::future::Future + Send + 'static,
+    F: Future + Send + 'static,
     F::Output: Send + 'static,
 {
     fn execute(&self, fut: F) {
@@ -41,56 +97,138 @@ where
     }
 }
 
-async fn serve_http2_connection(stream: TcpStream) -> Result<(), hyper::Error> {
-    let mut builder = hyper::server::conn::http2::Builder::new(TokioExecutor);
-    let http = builder.max_concurrent_streams(20);
-    let io = tokio::net::TcpStream::from_std(stream).unwrap();
-    http.serve_connection(io, service_fn(test)).await
+struct ServerHttp2 {
+    receiver: mpsc::Receiver<anyhow::Result<()>>,
 }
 
-pub async fn setup_http1(
-    future: impl std::future::Future<Output = anyhow::Result<()>>,
-) -> Result<(), anyhow::Error> {
-    static CELL_HTTP1: OnceLock<TcpListener> = OnceLock::new();
-    let listener = CELL_HTTP1.get_or_init(|| {
-        let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
-        TcpListener::bind(addr).unwrap()
-    });
+impl ServerHttp2 {
+    fn new() -> Self {
+        tracing::debug!("initializing http2 server");
+        static CELL_HTTP2: OnceLock<TcpListener> = OnceLock::new();
+        let listener = CELL_HTTP2.get_or_init(|| {
+            let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
+            tracing::debug!("preparing tcp listener at localhost:3001");
+            TcpListener::bind(addr).unwrap()
+        });
+        let (sender, receiver) = mpsc::channel::<anyhow::Result<()>>();
+        std::thread::spawn(move || {
+            tracing::debug!("dedicated thread to start listening");
+            match tokio::runtime::Builder::new_current_thread()
+                .enable_all()
+                .build()
+            {
+                Ok(rt) => {
+                    tracing::debug!("using tokio runtime");
+                    sender
+                        .send(rt.block_on(async move {
+                            tracing::debug!("preparing to accept incoming connection");
+                            let (stream, _) = listener.accept().map_err(anyhow::Error::from)?;
+                            tracing::trace!("tcp stream {:?}", stream);
 
-    let thread = tokio::task::spawn(async move {
-        let (stream, _) = listener.accept().unwrap();
-        let conn = serve_http1_connection(stream).await;
-        if let Err(err) = conn {
-            eprintln!("Error serving connection: {:?}", err);
-        }
-    });
+                            let mut builder =
+                                hyper::server::conn::http2::Builder::new(TokioExecutor);
+                            let http = builder.max_concurrent_streams(20);
+                            let io = tokio::net::TcpStream::from_std(stream)
+                                .map_err(anyhow::Error::from)?;
 
-    let (future_result, thread_result) = tokio::join!(future, thread);
-    future_result?;
-    thread_result.unwrap();
+                            tracing::debug!("preparing to bind connection to service");
+                            let conn = http.serve_connection(io, service_fn(test)).await;
+                            tracing::trace!("connection result {:?}", conn);
+                            if let Err(e) = &conn {
+                                let message = e.to_string();
+                                if message.contains("connection closed before reading preface")
+                                    || message.contains("unspecific protocol error detected")
+                                {
+                                    return Ok(());
+                                }
+                            }
+                            conn.map_err(anyhow::Error::from)
+                        }))
+                        .expect("value sent from http2 server dedicated thread");
+                }
+                Err(e) => {
+                    tracing::debug!("unable to start tokio runtime");
+                    sender.send(Err(anyhow::Error::from(e))).unwrap()
+                }
+            };
+        });
+        Self { receiver }
+    }
 
-    Ok(())
+    fn shutdown(self) -> anyhow::Result<()> {
+        tracing::debug!("shutting down http2 server");
+        self.receiver
+            .recv_timeout(DEFAULT_TIMEOUT)
+            .context("value received from http2 server dedicated thread")?
+    }
 }
 
-pub async fn setup_http2(
-    future: impl std::future::Future<Output = anyhow::Result<()>>,
-) -> anyhow::Result<()> {
-    static CELL_HTTP2: OnceLock<TcpListener> = OnceLock::new();
-    let listener = CELL_HTTP2.get_or_init(|| {
-        let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
-        TcpListener::bind(addr).unwrap()
-    });
-    let thread = tokio::task::spawn(async move {
-        let (stream, _) = listener.accept().unwrap();
-        let conn = serve_http2_connection(stream).await;
-        if let Err(err) = conn {
-            eprintln!("Error serving connection: {:?}", err);
-        }
+pub async fn setup_http1(f: impl Future<Output = anyhow::Result<()>>) -> anyhow::Result<()> {
+    tracing::debug!("preparing http1 server asynchronously");
+    let server = ServerHttp1::new();
+
+    tracing::debug!("running inner function (future)");
+    let result = f.await;
+
+    if let Err(err) = server.shutdown() {
+        tracing::error!("[host/server] failure {:?}", err);
+    }
+    result
+}
+
+pub fn setup_http1_sync<F>(f: F) -> anyhow::Result<()>
+where
+    F: FnOnce() -> anyhow::Result<()> + Send + 'static,
+{
+    tracing::debug!("preparing http1 server synchronously");
+    let server = ServerHttp1::new();
+
+    let (tx, rx) = mpsc::channel::<anyhow::Result<()>>();
+    tracing::debug!("running inner function in a dedicated thread");
+    std::thread::spawn(move || {
+        let _ = tx.send(f());
     });
+    let result = rx
+        .recv_timeout(DEFAULT_TIMEOUT)
+        .context("value received from request dedicated thread");
 
-    let (future_result, thread_result) = tokio::join!(future, thread);
-    future_result?;
-    thread_result.unwrap();
+    if let Err(err) = server.shutdown() {
+        tracing::error!("[host/server] failure {:?}", err);
+    }
+    result?
+}
+
+pub async fn setup_http2(f: impl Future<Output = anyhow::Result<()>>) -> anyhow::Result<()> {
+    tracing::debug!("preparing http2 server asynchronously");
+    let server = ServerHttp2::new();
 
-    Ok(())
+    tracing::debug!("running inner function (future)");
+    let result = f.await;
+
+    if let Err(err) = server.shutdown() {
+        tracing::error!("[host/server] Failure: {:?}", err);
+    }
+    result
+}
+
+pub fn setup_http2_sync<F>(f: F) -> anyhow::Result<()>
+where
+    F: FnOnce() -> anyhow::Result<()> + Send + 'static,
+{
+    tracing::debug!("preparing http2 server synchronously");
+    let server = ServerHttp2::new();
+
+    let (tx, rx) = mpsc::channel::<anyhow::Result<()>>();
+    tracing::debug!("running inner function in a dedicated thread");
+    std::thread::spawn(move || {
+        let _ = tx.send(f());
+    });
+    let result = rx
+        .recv_timeout(DEFAULT_TIMEOUT)
+        .context("value received from request dedicated thread");
+
+    if let Err(err) = server.shutdown() {
+        tracing::error!("[host/server] failure {:?}", err);
+    }
+    result?
 }
diff --git a/crates/test-programs/tests/wasi-http-components-sync.rs b/crates/test-programs/tests/wasi-http-components-sync.rs
new file mode 100644
index 000000000..cd3705d68
--- /dev/null
+++ b/crates/test-programs/tests/wasi-http-components-sync.rs
@@ -0,0 +1,174 @@
+#![cfg(all(feature = "test_programs", not(skip_wasi_http_tests)))]
+use wasmtime::{
+    component::{Component, Linker},
+    Config, Engine, Store,
+};
+use wasmtime_wasi::preview2::{
+    command::sync::{add_to_linker, Command},
+    pipe::MemoryOutputPipe,
+    IsATTY, Table, WasiCtx, WasiCtxBuilder, WasiView,
+};
+use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView};
+
+use test_programs::http_server::{setup_http1_sync, setup_http2_sync};
+
+lazy_static::lazy_static! {
+    static ref ENGINE: Engine = {
+        let mut config = Config::new();
+        config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
+        config.wasm_component_model(true);
+        let engine = Engine::new(&config).unwrap();
+        engine
+    };
+}
+// uses ENGINE, creates a fn get_module(&str) -> Module
+include!(concat!(env!("OUT_DIR"), "/wasi_http_tests_components.rs"));
+
+struct Ctx {
+    table: Table,
+    wasi: WasiCtx,
+    http: WasiHttpCtx,
+}
+
+impl WasiView for Ctx {
+    fn table(&self) -> &Table {
+        &self.table
+    }
+    fn table_mut(&mut self) -> &mut Table {
+        &mut self.table
+    }
+    fn ctx(&self) -> &WasiCtx {
+        &self.wasi
+    }
+    fn ctx_mut(&mut self) -> &mut WasiCtx {
+        &mut self.wasi
+    }
+}
+
+impl WasiHttpView for Ctx {
+    fn http_ctx(&self) -> &WasiHttpCtx {
+        &self.http
+    }
+    fn http_ctx_mut(&mut self) -> &mut WasiHttpCtx {
+        &mut self.http
+    }
+}
+
+fn instantiate_component(
+    component: Component,
+    ctx: Ctx,
+) -> Result<(Store<Ctx>, Command), anyhow::Error> {
+    let mut linker = Linker::new(&ENGINE);
+    add_to_linker(&mut linker)?;
+    wasmtime_wasi_http::proxy::sync::add_to_linker(&mut linker)?;
+
+    let mut store = Store::new(&ENGINE, ctx);
+
+    let (command, _instance) = Command::instantiate(&mut store, &component, &linker)?;
+    Ok((store, command))
+}
+
+fn run(name: &str) -> anyhow::Result<()> {
+    let stdout = MemoryOutputPipe::new();
+    let stderr = MemoryOutputPipe::new();
+    let r = {
+        let mut table = Table::new();
+        let component = get_component(name);
+
+        // Create our wasi context.
+        let mut builder = WasiCtxBuilder::new();
+        builder.stdout(stdout.clone(), IsATTY::No);
+        builder.stderr(stderr.clone(), IsATTY::No);
+        builder.arg(name);
+        for (var, val) in test_programs::wasi_tests_environment() {
+            builder.env(var, val);
+        }
+        let wasi = builder.build(&mut table)?;
+        let http = WasiHttpCtx::new();
+
+        let (mut store, command) = instantiate_component(component, Ctx { table, wasi, http })?;
+        command
+            .wasi_cli_run()
+            .call_run(&mut store)?
+            .map_err(|()| anyhow::anyhow!("run returned a failure"))?;
+        Ok(())
+    };
+    r.map_err(move |trap: anyhow::Error| {
+        let stdout = stdout.try_into_inner().expect("single ref to stdout");
+        if !stdout.is_empty() {
+            println!("[guest] stdout:\n{}\n===", String::from_utf8_lossy(&stdout));
+        }
+        let stderr = stderr.try_into_inner().expect("single ref to stderr");
+        if !stderr.is_empty() {
+            println!("[guest] stderr:\n{}\n===", String::from_utf8_lossy(&stderr));
+        }
+        trap.context(format!(
+            "error while testing wasi-tests {} with http-components-sync",
+            name
+        ))
+    })?;
+    Ok(())
+}
+
+#[test_log::test]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
+fn outbound_request_get() {
+    setup_http1_sync(|| run("outbound_request_get")).unwrap();
+}
+
+#[test_log::test]
+#[ignore = "test is currently flaky in ci and needs to be debugged"]
+fn outbound_request_post() {
+    setup_http1_sync(|| run("outbound_request_post")).unwrap();
+}
+
+#[test_log::test]
+#[ignore = "test is currently flaky in ci and needs to be debugged"]
+fn outbound_request_post_large() {
+    setup_http1_sync(|| run("outbound_request_post_large")).unwrap();
+}
+
+#[test_log::test]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
+fn outbound_request_put() {
+    setup_http1_sync(|| run("outbound_request_put")).unwrap();
+}
+
+#[test_log::test]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
+fn outbound_request_invalid_version() {
+    setup_http2_sync(|| run("outbound_request_invalid_version")).unwrap();
+}
+
+#[test_log::test]
+fn outbound_request_unknown_method() {
+    run("outbound_request_unknown_method").unwrap();
+}
+
+#[test_log::test]
+fn outbound_request_unsupported_scheme() {
+    run("outbound_request_unsupported_scheme").unwrap();
+}
+
+#[test_log::test]
+fn outbound_request_invalid_port() {
+    run("outbound_request_invalid_port").unwrap();
+}
+
+#[test_log::test]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
+fn outbound_request_invalid_dnsname() {
+    run("outbound_request_invalid_dnsname").unwrap();
+}
diff --git a/crates/test-programs/tests/wasi-http-components.rs b/crates/test-programs/tests/wasi-http-components.rs
index 4e0257cde..811b23151 100644
--- a/crates/test-programs/tests/wasi-http-components.rs
+++ b/crates/test-programs/tests/wasi-http-components.rs
@@ -5,7 +5,8 @@ use wasmtime::{
 };
 use wasmtime_wasi::preview2::{
     command::{add_to_linker, Command},
-    Table, WasiCtx, WasiCtxBuilder, WasiView,
+    pipe::MemoryOutputPipe,
+    IsATTY, Table, WasiCtx, WasiCtxBuilder, WasiView,
 };
 use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView};
 
@@ -60,7 +61,7 @@ async fn instantiate_component(
 ) -> Result<(Store<Ctx>, Command), anyhow::Error> {
     let mut linker = Linker::new(&ENGINE);
     add_to_linker(&mut linker)?;
-    wasmtime_wasi_http::add_to_component_linker(&mut linker)?;
+    wasmtime_wasi_http::proxy::add_to_linker(&mut linker)?;
 
     let mut store = Store::new(&ENGINE, ctx);
 
@@ -69,26 +70,54 @@ async fn instantiate_component(
 }
 
 async fn run(name: &str) -> anyhow::Result<()> {
-    let mut table = Table::new();
-    let component = get_component(name);
-
-    // Create our wasi context.
-    let wasi = WasiCtxBuilder::new()
-        .inherit_stdio()
-        .arg(name)
-        .build(&mut table)?;
-    let http = WasiHttpCtx::new();
-
-    let (mut store, command) = instantiate_component(component, Ctx { table, wasi, http }).await?;
-    command
-        .wasi_cli_run()
-        .call_run(&mut store)
-        .await
-        .map_err(|e| anyhow::anyhow!("wasm failed with {e:?}"))?
-        .map_err(|e| anyhow::anyhow!("command returned with failing exit status {e:?}"))
+    let stdout = MemoryOutputPipe::new();
+    let stderr = MemoryOutputPipe::new();
+    let r = {
+        let mut table = Table::new();
+        let component = get_component(name);
+
+        // Create our wasi context.
+        let mut builder = WasiCtxBuilder::new();
+        builder.stdout(stdout.clone(), IsATTY::No);
+        builder.stderr(stderr.clone(), IsATTY::No);
+        builder.arg(name);
+        for (var, val) in test_programs::wasi_tests_environment() {
+            builder.env(var, val);
+        }
+        let wasi = builder.build(&mut table)?;
+        let http = WasiHttpCtx::new();
+
+        let (mut store, command) =
+            instantiate_component(component, Ctx { table, wasi, http }).await?;
+        command
+            .wasi_cli_run()
+            .call_run(&mut store)
+            .await?
+            .map_err(|()| anyhow::anyhow!("run returned a failure"))?;
+        Ok(())
+    };
+    r.map_err(move |trap: anyhow::Error| {
+        let stdout = stdout.try_into_inner().expect("single ref to stdout");
+        if !stdout.is_empty() {
+            println!("[guest] stdout:\n{}\n===", String::from_utf8_lossy(&stdout));
+        }
+        let stderr = stderr.try_into_inner().expect("single ref to stderr");
+        if !stderr.is_empty() {
+            println!("[guest] stderr:\n{}\n===", String::from_utf8_lossy(&stderr));
+        }
+        trap.context(format!(
+            "error while testing wasi-tests {} with http-components",
+            name
+        ))
+    })?;
+    Ok(())
 }
 
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
 async fn outbound_request_get() {
     setup_http1(run("outbound_request_get")).await.unwrap();
 }
@@ -100,6 +129,18 @@ async fn outbound_request_post() {
 }
 
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+#[ignore = "test is currently flaky in ci and needs to be debugged"]
+async fn outbound_request_post_large() {
+    setup_http1(run("outbound_request_post_large"))
+        .await
+        .unwrap();
+}
+
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
 async fn outbound_request_put() {
     setup_http1(run("outbound_request_put")).await.unwrap();
 }
diff --git a/crates/test-programs/tests/wasi-http-modules.rs b/crates/test-programs/tests/wasi-http-modules.rs
index ba978c103..38dd25ffb 100644
--- a/crates/test-programs/tests/wasi-http-modules.rs
+++ b/crates/test-programs/tests/wasi-http-modules.rs
@@ -1,8 +1,9 @@
 #![cfg(all(feature = "test_programs", not(skip_wasi_http_tests)))]
 use wasmtime::{Config, Engine, Func, Linker, Module, Store};
 use wasmtime_wasi::preview2::{
+    pipe::MemoryOutputPipe,
     preview1::{WasiPreview1Adapter, WasiPreview1View},
-    Table, WasiCtx, WasiCtxBuilder, WasiView,
+    IsATTY, Table, WasiCtx, WasiCtxBuilder, WasiView,
 };
 use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView};
 
@@ -73,35 +74,61 @@ async fn instantiate_module(module: Module, ctx: Ctx) -> Result<(Store<Ctx>, Fun
 }
 
 async fn run(name: &str) -> anyhow::Result<()> {
-    let mut table = Table::new();
-    let module = get_module(name);
-
-    // Create our wasi context.
-    let wasi = WasiCtxBuilder::new()
-        .inherit_stdio()
-        .arg(name)
-        .build(&mut table)?;
-    let http = WasiHttpCtx::new();
-
-    let adapter = WasiPreview1Adapter::new();
-
-    let (mut store, command) = instantiate_module(
-        module,
-        Ctx {
-            table,
-            wasi,
-            http,
-            adapter,
-        },
-    )
-    .await?;
-    command
-        .call_async(&mut store, &[], &mut [wasmtime::Val::null()])
-        .await
-        .map_err(|e| anyhow::anyhow!("command returned with failing exit status {e:?}"))
+    let stdout = MemoryOutputPipe::new();
+    let stderr = MemoryOutputPipe::new();
+    let r = {
+        let mut table = Table::new();
+        let module = get_module(name);
+
+        // Create our wasi context.
+        let mut builder = WasiCtxBuilder::new();
+        builder.stdout(stdout.clone(), IsATTY::No);
+        builder.stderr(stderr.clone(), IsATTY::No);
+        builder.arg(name);
+        for (var, val) in test_programs::wasi_tests_environment() {
+            builder.env(var, val);
+        }
+        let wasi = builder.build(&mut table)?;
+        let http = WasiHttpCtx::new();
+
+        let adapter = WasiPreview1Adapter::new();
+
+        let (mut store, command) = instantiate_module(
+            module,
+            Ctx {
+                table,
+                wasi,
+                http,
+                adapter,
+            },
+        )
+        .await?;
+        command
+            .call_async(&mut store, &[], &mut [wasmtime::Val::null()])
+            .await
+    };
+    r.map_err(move |trap: anyhow::Error| {
+        let stdout = stdout.try_into_inner().expect("single ref to stdout");
+        if !stdout.is_empty() {
+            println!("[guest] stdout:\n{}\n===", String::from_utf8_lossy(&stdout));
+        }
+        let stderr = stderr.try_into_inner().expect("single ref to stderr");
+        if !stderr.is_empty() {
+            println!("[guest] stderr:\n{}\n===", String::from_utf8_lossy(&stderr));
+        }
+        trap.context(format!(
+            "error while testing wasi-tests {} with http-modules",
+            name
+        ))
+    })?;
+    Ok(())
 }
 
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
 async fn outbound_request_get() {
     setup_http1(run("outbound_request_get")).await.unwrap();
 }
@@ -113,6 +140,18 @@ async fn outbound_request_post() {
 }
 
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+#[ignore = "test is currently flaky in ci and needs to be debugged"]
+async fn outbound_request_post_large() {
+    setup_http1(run("outbound_request_post_large"))
+        .await
+        .unwrap();
+}
+
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+#[cfg_attr(
+    windows,
+    ignore = "test is currently flaky in ci and needs to be debugged"
+)]
 async fn outbound_request_put() {
     setup_http1(run("outbound_request_put")).await.unwrap();
 }
diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_post_large.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_post_large.rs
new file mode 100644
index 000000000..bacf9e1ec
--- /dev/null
+++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_post_large.rs
@@ -0,0 +1,40 @@
+use anyhow::{Context, Result};
+use std::io::{self, Read};
+use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme};
+
+struct Component;
+
+fn main() {}
+
+async fn run() -> Result<(), ()> {
+    const LEN: usize = 4000;
+    let mut buffer = [0; LEN];
+    io::repeat(0b001).read_exact(&mut buffer).unwrap();
+    let res = wasi_http_tests::request(
+        Method::Post,
+        Scheme::Http,
+        "localhost:3000",
+        "/post",
+        Some(&buffer),
+        None,
+    )
+    .await
+    .context("localhost:3000 /post large")
+    .unwrap();
+
+    println!("localhost:3000 /post large: {res:?}");
+    assert_eq!(res.status, 200);
+    let method = res.header("x-wasmtime-test-method").unwrap();
+    assert_eq!(std::str::from_utf8(method).unwrap(), "POST");
+    assert_eq!(res.body.len(), LEN);
+
+    Ok(())
+}
+
+impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component {
+    fn run() -> Result<(), ()> {
+        wasi_http_tests::in_tokio(async { run().await })
+    }
+}
+
+wasi_http_tests::export_command_extended!(Component);
diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs
index f144dd933..be10bc1d9 100644
--- a/tests/all/cli_tests.rs
+++ b/tests/all/cli_tests.rs
@@ -816,6 +816,28 @@ fn run_basic_component() -> Result<()> {
     Ok(())
 }
 
+#[cfg(feature = "wasi-http")]
+#[test]
+fn run_wasi_http_module() -> Result<()> {
+    let wasm = build_wasm("tests/all/cli_tests/wasi-http.wat")?;
+    let output = run_wasmtime_for_output(
+        &[
+            "--preview2",
+            "--wasi-modules",
+            "experimental-wasi-http",
+            "--disable-cache",
+            wasm.path().to_str().unwrap(),
+        ],
+        None,
+    )?;
+    println!("{}", String::from_utf8_lossy(&output.stderr));
+    assert!(String::from_utf8(output.stdout)
+        .unwrap()
+        .starts_with("Called _start\n"));
+    assert!(!output.status.success());
+    Ok(())
+}
+
 #[test]
 #[cfg_attr(not(feature = "component-model"), ignore)]
 fn run_precompiled_component() -> Result<()> {
diff --git a/tests/all/cli_tests/wasi-http.wat b/tests/all/cli_tests/wasi-http.wat
new file mode 100644
index 000000000..9f5d51979
--- /dev/null
+++ b/tests/all/cli_tests/wasi-http.wat
@@ -0,0 +1,93 @@
+(module
+  (import "wasi_snapshot_preview1" "proc_exit"
+    (func $__wasi_proc_exit (param i32)))
+  (import "wasi:io/streams" "write"
+    (func $__wasi_io_streams_write (param i32 i32 i32 i32)))
+  (import "wasi:io/streams" "blocking-write"
+    (func $__wasi_io_streams_blocking_write (param i32 i32 i32 i32)))
+  (import "wasi:io/streams" "subscribe-to-output-stream"
+    (func $__wasi_io_streams_subscribe_to_output_stream (param i32) (result i32)))
+  (import "wasi:http/types" "new-fields"
+    (func $__wasi_http_types_new_fields (param i32 i32) (result i32)))
+  (import "wasi:http/types" "drop-fields"
+    (func $__wasi_http_types_drop_fields (param i32)))
+  (import "wasi:http/types" "new-outgoing-request"
+    (func $__wasi_http_types_new_outgoing_request (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
+  (import "wasi:http/types" "outgoing-request-write"
+    (func $__wasi_http_types_outgoing_request_write (param i32 i32)))
+  (import "wasi:http/types" "drop-outgoing-request"
+    (func $__wasi_http_types_drop_outgoing_request (param i32)))
+  (func $_start
+    (local i32)
+    (local $headers_id i32)
+    (local $request_id i32)
+    (local $body_stream_id i32)
+
+    ;; Print "Called _start".
+    (call $print (i32.const 32) (i32.const 14))
+
+    (local.set $headers_id (call $__wasi_http_types_new_fields
+      i32.const 0 ;; base pointer
+      i32.const 0 ;; length
+    ))
+    (local.set $request_id (call $__wasi_http_types_new_outgoing_request
+      i32.const 0 ;; method = Method::Get
+      i32.const 0 ;; method pointer
+      i32.const 0 ;; method length
+      i32.const 0 ;; path is some = None
+      i32.const 0 ;; path pointer
+      i32.const 0 ;; path length
+      i32.const 1 ;; scheme is some = Some
+      i32.const 1 ;; scheme = Scheme::Https
+      i32.const 0 ;; scheme pointer
+      i32.const 0 ;; scheme length
+      i32.const 1 ;; authority is some = Some
+      i32.const 96 ;; authority pointer = Constant value
+      i32.const 15 ;; authority length
+      local.get $headers_id ;; headers id
+    ))
+    (call $__wasi_http_types_outgoing_request_write (local.get $request_id) (local.get 0))
+    local.get 0
+    i32.const 4
+    i32.add
+    i32.load
+    local.set $body_stream_id
+    (call $__wasi_io_streams_write
+      (local.get $body_stream_id) ;; body stream id (usually 8)
+      (i32.const 128) ;; body stream pointer
+      (i32.const 4) ;; body stream length
+      (i32.const 0)
+    )
+    (drop (call $__wasi_io_streams_subscribe_to_output_stream (local.get $body_stream_id)))
+    (call $__wasi_http_types_drop_fields (local.get $headers_id))
+    (call $__wasi_http_types_drop_outgoing_request (local.get $request_id))
+
+    (call $print (i32.const 64) (i32.const 5))
+    (drop (call $__wasi_io_streams_subscribe_to_output_stream (i32.const 4)))
+
+    (call $__wasi_proc_exit (i32.const 1))
+  )
+
+  ;; A helper function for printing ptr-len strings.
+  (func $print (param $ptr i32) (param $len i32)
+    (call $__wasi_io_streams_blocking_write
+      i32.const 4 ;; Value for stdout
+      local.get $ptr
+      local.get $len
+      i32.const 0
+    )
+  )
+
+  (func $cabi_realloc (param i32 i32 i32 i32) (result i32)
+    i32.const 0
+  )
+
+  (memory 1)
+  (export "memory" (memory 0))
+  (export "_start" (func $_start))
+  (export "cabi_realloc" (func $cabi_realloc))
+  (data (i32.const 32) "Called _start\0a")
+  (data (i32.const 64) "Done\0a")
+  (data (i32.const 96) "www.example.com")
+  (data (i32.const 128) "body")
+)