Skip to content

fix: patch global Response with a proxy instead of a subclass - #984

Open
philmillman wants to merge 2 commits into
mainfrom
fix-response-patch-srvx-compat
Open

fix: patch global Response with a proxy instead of a subclass#984
philmillman wants to merge 2 commits into
mainfrom
fix-response-patch-srvx-compat

Conversation

@philmillman

Copy link
Copy Markdown
Member

Fixes #983

The problem

@preventLeak broke srvx-based servers (TanStack Start's FastResponse, Nitro) with:

TypeError: Cannot read private member #state from an object whose class did not declare it
    at NodeResponse.get body [as body] (node:internal/deps/undici/undici:9767:21)

Root cause

Not srvx-specific in principle: it's a conflict between patchGlobalResponse() and any library that reflects over Response.prototype.

The patch replaced globalThis.Response with a subclass, and a subclass's .prototype owns nothing but constructor. All the real members (body, text(), clone(), bodyUsed, ...) live one level up on the native prototype.

srvx's node adapter snapshots globalThis.Response at import time and builds its FastResponse via lazyInherit(), which walks only Object.getOwnPropertyNames(source). Against the subclass prototype it finds nothing, forwards nothing, then does Object.setPrototypeOf(NodeResponse.prototype, NativeResponse.prototype). So fastResponse.body falls straight through to undici's native getter with a NodeResponse receiver.

Ordering is exactly what the Vite integration produces: it patches at config-load time, well before the dev server imports srvx.

Node version changes the message, not the bug: Node 24's undici uses #state (the reported error), Node 22 uses a symbol and reports Cannot read properties of undefined (reading 'body').

The fix

Wrap the native class in a Proxy with a construct trap instead of subclassing it. Response.prototype stays the real native prototype, so reflection-based wrappers see what they expect, and every instance is a genuine native Response with its internal slots intact.

Two cleanups fall out of this:

Leak detection is unchanged

Verified end-to-end against a live srvx server:

  • plain new Response(secret) -> caught by the construct trap
  • srvx's fast path (bypasses Response entirely, writes straight to nodeRes.write) -> still caught by patchGlobalServerResponse
  • srvx's own internal new NativeResponse(...) -> now routes through the trap too

Before/after on the same server, Node 24:

BEFORE  GET /middleware -> 500  TypeError: Cannot read private member #state ...
AFTER   GET /middleware -> 200  "<html>from inner handler</html>"

Tests

Four regression tests added. The key one asserts Response.prototype keeps its native own-property set after patching, which is the invariant srvx depends on; the others cover native instance construction, subclassing, and Response.json.

For reviewers

I could not scaffold a real TanStack Start app on Windows to confirm there is no second failure behind this one. The srvx incompatibility is real and fixed, but if the reporter still hits an error after this ships, it would be a separate issue.

Replacing globalThis.Response with a subclass leaves Response.prototype
owning nothing but `constructor`. Libraries that reflect over the
prototype's own properties to build their own Response-alike then wire up
nothing and fall through to the native getters with a foreign `this`.

srvx's node adapter does exactly this (lazyInherit + setPrototypeOf), so
its FastResponse threw "Cannot read private member #state from an object
whose class did not declare it" on any native-inherited member. That
broke TanStack Start dev servers using srvx as the response handler.

Wrapping the native class in a Proxy keeps Response.prototype pointing at
the real native prototype and hands back genuine native instances, while
the construct trap still scans bodies for leaks. This also removes the
need for the Symbol.hasInstance override and the setPrototypeOf call in
Response.json, since instanceof and the returned prototype are now
correct on their own.

Fixes #983
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

patch Patch releases

  • varlock 1.16.0 → 1.16.1

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size

⚠️ grows the bundle by 2.6 KB (+0.1%)

Metric main This PR Δ
Total dist 5007.0 KB 5009.5 KB +2.6 KB (+0.1%)
JS 1716.6 KB 1717.6 KB +1.1 KB (+0.1%)
Sourcemaps 3213.8 KB 3215.3 KB +1.5 KB (+0.0%)
Type defs 76.6 KB 76.6 KB

dist/ only; native binaries are versioned separately and not counted here.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The constructor proxy fixes native prototype reflection, but its Response.json interception introduces a concrete compatibility regression that should be addressed before merging.

Reviewed changes in 56749fc7, covering the complete three-file patch and the affected runtime behavior.

  • Native prototype preservation: Replaces the patched subclass with a constructor proxy so reflection-based response implementations retain access to native prototype members.
  • Leak scanning hooks: Keeps constructor-body scanning, virtualizes the patch marker, and routes static Response.json reads through a scanner.
  • Regression coverage and release metadata: Adds focused tests for reflection, native instances, subclasses, and JSON responses, plus a patch changeset.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using azure/gpt-5.6-sol𝕏

Comment thread packages/varlock/src/runtime/patch-response.ts Outdated
@pkg-pr-new

pkg-pr-new Bot commented Aug 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/varlock@984

commit: effbb90

@philmillman
philmillman requested a review from theoephraim August 5, 2026 14:05
@pullfrog

pullfrog Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Pushed commit effbb900 to PR #984. Response.json now calls a stable captured native implementation while respecting later assignments and non-configurable replacements; focused coverage passes all 10 tests, lint passes, and the review thread is resolved.

Task list (5/5 completed)

Pullfrog  | View workflow run | via Pullfrog | Using azure/gpt-5.6-sol𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: @preventLeak errors when using servx as Tanstack Start's response handler

1 participant