Skip to content

service: spawn body reader via wasm-bindgen-futures on wasm32#63

Open
connyay wants to merge 4 commits intoanthropics:mainfrom
connyay:wasm32-bidi-spawn-local
Open

service: spawn body reader via wasm-bindgen-futures on wasm32#63
connyay wants to merge 4 commits intoanthropics:mainfrom
connyay:wasm32-bidi-spawn-local

Conversation

@connyay
Copy link
Copy Markdown

@connyay connyay commented Apr 27, 2026

spawn_body_reader runs the per-request envelope decoder in the background so bidi and client-streaming RPCs can read incoming messages while the handler is producing responses. It used tokio::spawn, which panics with "there is no reactor running, must be called from the context of a Tokio 1.x runtime" on wasm32-unknown-unknown targets (Cloudflare Workers, etc.), where the ambient executor is wasm-bindgen-futures rather than tokio. Server-streaming and unary RPCs worked on Workers, but bidi and client-streaming panicked the moment the first envelope was decoded.

Under cfg(target_arch = "wasm32"), dispatch the reader future via wasm_bindgen_futures::spawn_local instead. spawn_local doesn't return a joinable handle, but the existing code only held the JoinHandle for lifetime association (don't abort early), so None works fine. spawn_body_reader's return type and
StreamingResponseBody::with_reader_task now take Option<JoinHandle<()>>. tokio::sync::mpsc keeps working unchanged; its internals only need wakers, which wasm-bindgen-futures provides.

Adds a wasm32-only target dep on wasm-bindgen-futures = "0.4". No behaviour change on native targets; the tokio path is unchanged.

Verified end-to-end against a Cloudflare Worker via miniflare and wrangler dev: bidi calls now succeed, the handler is dispatched before the request body completes, and per-input echoes round-trip in ~1ms while the request stream is still open. See
https://github.com/connyay/example-connectrpc-worker for a runnable example (clock = server-stream, echo = bidi, heartbeat = full-duplex bidi probe).

Scope here is server-side handler dispatch. call_bidi_stream in client/mod.rs also calls tokio::spawn, but the rest of the client module already requires hyper / tokio-net features that don't compile on wasm32. Wasm client transports are a separate concern handled by user-supplied ClientTransport impls (see examples/wasm-client).

`spawn_body_reader` runs the per-request envelope decoder in the
background so bidi and client-streaming RPCs can read incoming messages
while the handler is producing responses. It used `tokio::spawn`, which
panics with "there is no reactor running, must be called from the
context of a Tokio 1.x runtime" on `wasm32-unknown-unknown` targets
(Cloudflare Workers, etc.), where the ambient executor is
`wasm-bindgen-futures` rather than tokio. Server-streaming and unary
RPCs worked on Workers, but bidi and client-streaming panicked the
moment the first envelope was decoded.

Under `cfg(target_arch = "wasm32")`, dispatch the reader future via
`wasm_bindgen_futures::spawn_local` instead. `spawn_local` doesn't
return a joinable handle, but the existing code only held the
`JoinHandle` for lifetime association (don't abort early), so `None`
works fine. `spawn_body_reader`'s return type and
`StreamingResponseBody::with_reader_task` now take
`Option<JoinHandle<()>>`. `tokio::sync::mpsc` keeps working unchanged;
its internals only need wakers, which `wasm-bindgen-futures` provides.

Adds a wasm32-only target dep on `wasm-bindgen-futures = "0.4"`. No
behaviour change on native targets; the tokio path is unchanged.

Verified end-to-end against a Cloudflare Worker via miniflare and
`wrangler dev`: bidi calls now succeed, the handler is dispatched
before the request body completes, and per-input echoes round-trip in
~1ms while the request stream is still open. See
https://github.com/connyay/example-connectrpc-worker for a runnable
example (clock = server-stream, echo = bidi, heartbeat = full-duplex
bidi probe).

Scope here is server-side handler dispatch. `call_bidi_stream` in
`client/mod.rs` also calls `tokio::spawn`, but the rest of the `client`
module already requires hyper / tokio-net features that don't compile
on wasm32. Wasm client transports are a separate concern handled by
user-supplied `ClientTransport` impls (see `examples/wasm-client`).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@connyay
Copy link
Copy Markdown
Author

connyay commented Apr 27, 2026

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Apr 27, 2026
connyay and others added 2 commits April 28, 2026 11:35
Replace the two inline cfg(target_arch) directives in spawn_body_reader
with a single helper that owns the platform fork. The native variant
takes Send + 'static and forwards to tokio::spawn; the wasm32 variant
takes 'static and forwards to wasm_bindgen_futures::spawn_local,
returning None for the join handle.

The call site in spawn_body_reader is now cfg-free. The wasm hedge
also drops out of with_reader_task's docstring; the Send-bound
divergence and the no-abort policy live next to spawn_detached.
@iainmcgin
Copy link
Copy Markdown
Collaborator

[claude code] Pushed a small fixup commit (d0ce6b0) extracting a spawn_detached helper to centralise the platform fork. The two inline #[cfg(target_arch = "wasm32")] blocks in spawn_body_reader are replaced with a single let reader_task = spawn_detached(reader_future); call, with the cfg gates moved onto two definitions of the helper (one with the Send + 'static bound for tokio::spawn, one with just 'static for spawn_local). The wasm hedge in with_reader_task's docstring also drops out — the platform contract now lives in one place.

Verified locally:

  • cargo check -p connectrpc (native) — clean
  • cargo check -p connectrpc --target wasm32-unknown-unknown --no-default-features — clean
  • cargo test -p connectrpc --lib — 235/235 pass
  • cargo clippy -p connectrpc --all-targets -- -D warnings — clean
  • cargo doc --document-private-items — same warning count as baseline (9 pre-existing intra-doc-link warnings unrelated to this PR)

Happy to revert if you'd rather keep the original shape, but this gets the diff to a place we're comfortable approving and merging.

@connyay
Copy link
Copy Markdown
Author

connyay commented Apr 30, 2026

The fixup looks great, thanks!

iainmcgin added a commit that referenced this pull request Apr 30, 2026
Bumps `iainmcgin/cla-github-action` to `73f6929` and allowlists
`noreply@anthropic.com`.

The new revision adds email-based matching to the allowlist alongside
the existing name-based match. AI assistants commonly add
`Co-authored-by:` trailers like `Claude Opus 4.7
<noreply@anthropic.com>`, and the impersonation guard added in the
previous bump correctly counts those as committers — without an email
allowlist, every PR with such a trailer fails the CLA check. The new
entry suppresses the synthetic identity once. The PR opener still has to
sign.

Tests on the action: 100/100 pass, including new cases for exact email
match, `*@anthropic.com` glob, and the no-email-supplied path.

Caught while reviewing PR #63 — connyay's commit there carries a
`Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>` trailer that
was failing the CLA check after the previous bump landed on main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants