fix: keep auto-mode route handler request re-wrappable by fetch handler libraries (oRPC/Hono) on Workers#2558
Open
exKAZUu wants to merge 2 commits into
Open
Conversation
App Router route handlers routinely pass their request to fetch-based handler libraries (oRPC, Hono, …) that re-wrap it via `new Request(request, init)`. The tracked request was a Proxy, which is not a real Request: on workerd `new Request(proxy)` cannot read the native internal slots and coerces the proxy to the URL string "[object Request]", so the library's `new URL(request.url)` throws `TypeError: Invalid URL: [object Request]` (undici throws "Cannot read private member #state"). This crashed every mutation routed through such a handler in production. In "auto" mode, track dynamic access with own accessor properties on the real NextRequest instead of a Proxy, keeping the object a genuine, re-wrappable Request. A reentrancy guard prevents getters that read other tracked props internally (e.g. ip/geo reading headers) from double-reporting. Value-substituting static modes keep using the Proxy.
…tionale Per AGENTS.md, changesets are generated by CI from Conventional Commit subjects; hand-authored `.changeset/*.md` files must not be committed. Also document why force-static/error modes keep the Proxy: own accessors only shadow JS getters while `new Request(req)` copies native slots, so stubbing those modes would leak the real URL/headers/cookies/body on re-wrap. Keeping the Proxy makes re-wrap throw instead of leak.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2557
Problem
App Router route handlers routinely hand the incoming
requestto fetch-based handler libraries — oRPC (RPCHandler/BodyLimitPlugin), Hono, etc. — which re-wrap it vianew Request(request, init). On Cloudflare Workers this crashes with:and returns an empty
500. In a real app this silently broke every mutation (each routed through one oRPC/api/rpcroute handler) after migrating to vinext.Root cause
createTrackedAppRouteRequest()hands the handler aProxywrapping aNextRequest(to track dynamic access). AProxyis not a genuineRequest, so re-wrapping it fails:new Request(proxy, init)can't read the native internal slots, so it coerces the proxy to a URL string.String(proxy)is"[object Request]"(viaSymbol.toStringTag), so the re-wrapped request'surlbecomes"[object Request]"and the library's laternew URL(request.url)throwsTypeError: Invalid URL: [object Request].TypeError: Cannot read private member #state from an object whose class did not declare it.proxy.urlon its own is fine; the breakage only happens on re-wrap, which these libraries do routinely.Fix
In the common
"auto"(dynamic) mode, install dynamic-access tracking via own accessor properties on the realNextRequestinstead of returning aProxy. The handler now receives a genuine, re-wrappableRequest. Native-slot reads duringnew Request(req, init)bypass JS getters, so tracking is unchanged.A small reentrancy guard prevents getters that internally read another tracked property (e.g. the shim's
ip/georeadingthis.headers) from double-reporting — matching the previous behaviour where the Proxy read through the untracked target.The value-substituting static-generation modes (
force-static,error) intentionally return different values than the underlying request and are not meant to be re-wrapped, so they keep using the Proxy.Tests
Added two regression tests to
tests/app-route-handler-runtime.test.ts:stays re-wrappable via new Request(request, init) for fetch handler libraries— mirrors oRPC'sBodyLimitPlugin(new Request(request, { body, duplex })) and assertsurl, method, headers and body survive. This fails before the change (undici throwsCannot read private member #state).keeps a GET route handler request re-wrappable without a body.All existing tests in the file continue to pass (dynamic tracking,
ip/geo,nextUrl, body-reading, force-static and error modes).Verification
pnpm exec vitest run --project unit tests/app-route-handler-runtime.test.ts→ 15/15 passtests/app-route-handler-execution.test.ts+tests/app-route-handler-cache.test.ts→ 15/15 passpnpm exec vp checkon the changed files → no warnings, lint errors, or type errorswrangler dev) and Node v26.A changeset is included (
vinext: patch).