Skip to content

fix: stop leaking the client selector into RequestInit (breaks Deno / edge runtimes) - #8

Closed
henrikra wants to merge 1 commit into
zernio-dev:mainfrom
henrikra:fix/deno-request-client-leak
Closed

henrikra wants to merge 1 commit into
zernio-dev:mainfrom
henrikra:fix/deno-request-client-leak

Conversation

@henrikra

Copy link
Copy Markdown

Summary

Every SDK call throws under Deno and other WinterCG runtimes (Supabase Edge Functions, Deno Deploy, Cloudflare Workers):

TypeError: Failed to construct 'Request': Argument 2 `client` must be a Deno.HttpClient

It fails before any network I/O, so it looks like a total outage of the SDK on those runtimes. It works on Node, which is why the current test suite doesn't catch it.

Root cause

_bind (src/client.ts) injects this instance's client under the client key so each operation runs on the right per-instance client:

const withClient = { ...options };
if (withClient['client'] === undefined) {
  withClient['client'] = this._client;
}
return operation(withClient);

The generated operation then does (options?.client ?? client).get({ ...options, url }), and @hey-api/client-fetch@0.6 spreads that whole options object — client included — straight into the fetch RequestInit (new Request(url, { ...init, client })).

  • Node (undici): silently ignores unknown RequestInit fields → the stray client is harmless → tests stay green.
  • Deno / WinterCG: new Request validates client as a Deno.HttpClient and rejects the hey-api client object → every call throws.

This matters because the SDK's own docs target edge runtimes, and it's currently unusable there (confirmed on 0.2.315 → present through 0.2.540).

Fix

The client selector has already done its job (picking the per-instance client) by the time the HTTP method runs, so strip it from the options before hey-api builds the request. One localized change in the constructor; per-instance isolation (auth interceptor, baseURL, defaultHeaders) is unaffected.

Tests

  • Added tests/request-init.test.ts — asserts the client selector never reaches the RequestInit, captured at new Request time (undici would otherwise swallow it and hide the bug). Fails on the previous behaviour, passes with this change.
  • Full suite green: npm test → 177 passed (including the existing isolation.test.ts).
  • Verified under the actual failing runtime: a Deno script calling client.accounts.listAccounts() against a stubbed fetch throws before this change and succeeds after.

Notes for maintainers

  • This is a minimal, dependency-free fix. Alternatives you might prefer: bumping @hey-api/client-fetch to a version that no longer spreads non-RequestInit keys, or having _bind avoid the client field entirely.
  • The gap is environmental: the Node-only test matrix can't observe this. Consider adding a small Deno (or workerd) job that runs one real call against a stub fetch — it would guard this whole class of runtime breakage going forward. Happy to add that here if you'd like.

…dge)

Every SDK call throws under Deno and other WinterCG runtimes:

    TypeError: Failed to construct 'Request':
    Argument 2 `client` must be a Deno.HttpClient

`_bind` injects the per-instance client under the `client` key so each
operation runs on the right client. @hey-api/client-fetch then spreads the
whole per-request options object — `client` included — into the fetch
`RequestInit`. Node's undici ignores unknown init fields, so the stray
`client` is harmless there and the test suite (Node) stays green. Deno's
`new Request` validates `client` as a `Deno.HttpClient` and rejects the
hey-api client object, so every call fails before any network I/O on Deno
Deploy, Supabase Edge Functions, Cloudflare Workers, etc.

The `client` selector has already done its job by the time the HTTP method
runs, so strip it from the options before the request is built. Per-instance
isolation (auth interceptor, baseURL, defaultHeaders) is unaffected.

Add tests/request-init.test.ts: asserts the selector never reaches the
RequestInit (captured at `new Request` time, since undici would otherwise
swallow it). Fails on the previous behaviour, passes now.

Verified: `npm test` (177 tests) passes; a Deno smoke calling
`accounts.listAccounts` against a stubbed fetch throws before this change and
succeeds after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Ooscaar

Ooscaar commented Aug 10, 2026

Copy link
Copy Markdown
Member

@henrikra
Thanks for the PR.
I merged it on #9, but the functionality is exactly the same one.

Thanks!

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