Skip to content

fix: compile generated clients (incl. SSE + retry) under wasm32 (#74) - #75

Merged
lightsofapollo merged 4 commits into
mainfrom
fix/issue-74-wasm-generated-client
Sep 14, 2026
Merged

lightsofapollo merged 4 commits into
mainfrom
fix/issue-74-wasm-generated-client

Conversation

@lightsofapollo

@lightsofapollo lightsofapollo commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

What

Fixes #74. Generated HTTP clients now compile for wasm32-unknown-unknown,
including the opt-in SSE runtime and retry middleware.

Default client. The bounded response reader emitted
reqwest::Response::chunk(), which exists only on reqwest's native backend. The
wasm backend exposes json/text/bytes/bytes_stream but no chunk, so
every generated client failed under trunk serve with no method named chunk found for struct Response.

SSE runtime. enable_sse_client + [[streaming.endpoints]] emitted
Pin<Box<dyn Stream + Send>> and bare #[async_trait] (which implies Send
futures). reqwest's wasm fetch body is !Send, so those signatures cannot
work. It now emits a cfg-split alias and attribute:

#[cfg(not(target_arch = "wasm32"))]
pub type BoxSseStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
#[cfg(target_arch = "wasm32")]
pub type BoxSseStream<T> = Pin<Box<dyn Stream<Item = T>>>;

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]

Native keeps Send streams and the public API is unchanged there. The
reconnect path now holds the body stream instead of a Response, dropping the
last native-only chunk().

Retry. [http_client.retry] pulls retry-policiesrand
getrandom 0.4, which rejects wasm32 without wasm_js. SSE timers need
futures-timer/wasm-bindgen. Both are emitted target-scoped:

[target.'cfg(target_arch = "wasm32")'.dependencies]
futures-timer = { version = "3", features = ["wasm-bindgen"] }
getrandom = { version = "0.4", features = ["wasm_js"] }

DepRequirement gains a target field; REQUIRED_DEPS.toml gains
futures-util globally and the target table. The native dependency set is
unchanged.

Verification

  • Full suite: 680 passed, 0 failed, 6 skipped.
  • scripts/corpus-manifest.sh --check passes (corpus config uses neither opt-in
    stack, so the manifest is unchanged by these additions).
  • generated_wasm_client_test compiles default, retry, SSE, and SSE+retry
    configurations for both wasm32 and native from the exact emitted fragment.
  • Generated OpenAI client with SSE and retry enabled compiles for native and
    wasm32 with zero errors.
  • The live SSE transport test still passes end to end against an
    OpenAI/Anthropic-compatible backend.
  • Fixed examples/server-openai-responses, whose checked-in manifest was
    missing reqwest's stream feature (caught by the openai-sdk-compat job).

The generated bounded response reader called
`reqwest::Response::chunk()`, which exists only on reqwest's native
backend. reqwest's wasm backend exposes `json`/`text`/`bytes`/
`bytes_stream` but no `chunk`, so every generated client failed under
`trunk serve` with `no method named chunk found for struct Response`.

Buffer through `bytes_stream()` instead, which is available on both
targets behind reqwest's `stream` feature (already requested by the
generated dependency fragment). This moves emitted output for every
spec that generates a client, and the fragment gains `futures-util`.

Add `generated_wasm_client_test`, which compiles a generated client for
wasm32 on CI; the `test` job installs the target so it cannot silently
skip. Update the scratch manifests in tests that hardcode dependencies
to match the new fragment.

Verified against the real OpenAI and Anthropic clients: both compile for
wasm32, and a wasm-bindgen app around each completed live requests
(`list_models`, `create_chat_completion`, `messages_post`) against an
OpenAI/Anthropic-compatible backend.

The opt-in SSE runtime and opt-in retry middleware remain non-wasm32 and
are tracked separately.
@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openapi-to-rust Ready Ready Preview Sep 14, 2026 7:09am UTC

Request Review

Follow-up to the default-client fix. The opt-in SSE runtime and retry
middleware still failed on wasm32:

* SSE emitted `Pin<Box<dyn Stream + Send>>` and bare `#[async_trait]`
  (which implies `Send` futures). reqwest's wasm `fetch` body is `!Send`,
  so no amount of bounding makes those signatures work. Emit a cfg-split
  `BoxSseStream<T>` alias and `#[cfg_attr(..., async_trait(?Send))]`:
  native keeps `Send` streams and the public API is unchanged there,
  wasm is single-threaded. The reconnect state holds the body stream
  (rather than a `Response`) and no longer calls the native-only
  `Response::chunk()`.

* retry pulls `retry-policies` -> `rand` -> `getrandom 0.4`, which
  `compile_error!`s on wasm32 without the `wasm_js` feature. Emit a
  target-scoped `getrandom` dependency when retry is configured.

* SSE timers need `futures-timer/wasm-bindgen` on wasm32, also emitted
  target-scoped.

`DepRequirement` gains `target`, rendered under
`[target.'cfg(target_arch = "wasm32")'.dependencies]`. The native
dependency set is unchanged. `generated_wasm_client_test` now covers
default, retry, SSE, and SSE+retry on both wasm32 and native.

Verified the generated OpenAI client with SSE and retry enabled compiles
for native and wasm32 with zero errors, and the live SSE transport test
still passes end to end.
@lightsofapollo lightsofapollo changed the title fix: compile generated clients under wasm32 (#74) fix: compile generated clients (incl. SSE + retry) under wasm32 (#74) Sep 14, 2026
`corpus_build` builds the base and head generators into one shared target
dir so dependencies compile once, and both resolve to the same friendly
`<target>/<profile>/openapi-to-rust` path. With a restored `rust-cache`,
cargo can consider the requested side fresh and skip relinking, leaving
that path holding the other side's binary. `gen-diff` then generated the
"head" corpus with the base generator and reported "no change" while the
manifest check correctly saw stale output.

Delete the friendly path before building so the missing output marks the
unit dirty, and fail loudly if it is still absent afterward. This makes
the corpus diff reliable on a warm cache.
`gen-diff.sh` built the base and head generators into the same target
dir. Both are the same `openapi-to-rust` package, so they occupy the same
cargo fingerprint/artifact slots. With a restored `rust-cache`, the head
crate could look fresh after the base build overwrote those slots: cargo
skipped relinking, and the "head" corpus was generated by the base binary.
`gen-diff` then reported "no change" while the manifest gate correctly saw
stale output.

Build the base side with `--target-dir target/gen-diff-base` so each
side's artifact is independent. The dir lives under `target/`, so
rust-cache keeps it warm across runs; dependencies compile once per dir.

Revert the earlier `rm -f` workaround in `corpus_build`, which did not
help because cargo restores the friendly path from the collided artifact.
@lightsofapollo
lightsofapollo merged commit a912d28 into main Sep 14, 2026
14 checks passed
@lightsofapollo
lightsofapollo deleted the fix/issue-74-wasm-generated-client branch September 14, 2026 17:33
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.

[bug]: generated client doesn't compile under WASM

1 participant