cloudflare / cloudflare/vinext
Route handler request (a Proxy) breaks fetch handler libraries that re-wrap it via new Request() on Workers
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Summary
In an App Router **route handler** (`route.ts`), passing the incoming `request` to any fetch-based handler library that re-wraps it via `new Request(request, init)` crashes on Cloudflare Workers. This is a very common pattern — oRPC's `RPCHandler` (its `BodyLimitPlugin` does `new Request(request, { body, duplex })`), Hono's adapter, etc.
On workerd the failure surfaces as:
```
TypeError: Invalid URL: [object Request]
```
logged by vinext as `[vinext] Route handler error: ...` and returned to the client as an empty `500`.
## Root cause
`createTrackedAppRouteRequest()` (`packages/vinext/src/server/app-route-handler-runtime.ts`) hands the route handler a **`Proxy`** wrapping a `NextRequest`, to track dynamic request access.
A `Proxy` is not a genuine `Request`. When a handler library does `new Request(theProxy, init)`:
- On **workerd**, `new Request()` cannot read the native internal slots of a Proxy, so it coerces the argument to a string URL. `String(proxy)` is `"[object Request]"` (via `Symbol.toStringTag`), so the re-wrapped request's `url` becomes `"[object Request]"`. The library's subsequent `new URL(request.url)` then throws `TypeError: Invalid URL: [object Request]`.
- On **Node (undici)** the same call throws `TypeError: Cannot read private member #state from an object whose class did not declare it`.
`.url` on the proxy itself is fine (the getter returns the correct string); the breakage only happens when the request is **re-wrapped** into a new `Request`, which fetch handler libraries routinely do.
## Minimal reproduction (workerd)
```js
// worker.mjs — run with `wrangler dev`
import { createTrackedAppRouteRequest } from "vinext/dist/server/app-route-handler-runtime.js";
export default {
async fetch(request) {
const tracked = createTrackedAppRouteRequest(request, {}).request;
// Exactly what oRPC's BodyLimitPlugin / Hono / etc. do internally:
const rewrapped = new Request(tracked, { body: tracked.body, duplex: "half" });
return Response.json({ url: rewrapped.url }); // -> TypeError: Invalid URL: [object Request]
},
};
```
`POST` any body to it → `500`, worker log shows `TypeError: Invalid URL: [object Request]`.
The same `new Request(tracked, init)` throws `Cannot read private member #state` under Node's undici, so it also reproduces in a plain Node unit test.
## Impact
Any App Router route handler that delegates to oRPC / Hono / other fetch-router libraries returns `500` for every request on Workers. In our app this silently broke **all** mutations (record, like, bookmark, …) after migrating to vinext, since every one goes through a single oRPC `/api/rpc` route handler.
## Suggested fix
Return a **real `Request`** from `createTrackedAppRouteRequest()` in the common `"auto"` mode — install dynamic-access tracking via own accessor properties on the actual `NextRequest` instead of a `Proxy`, so the object stays a genuine, re-wrappable `Request`. (Native-slot reads during `new Request(req, init)` bypass JS getters, so tracking is unaffected.) The value-substituting static-generation modes (`force-static`, `error`) can keep using the Proxy since those requests are not meant to be re-wrapped.
I've opened a PR implementing this with regression tests.
## Environment
- vinext `1.0.0-beta.0`, `@vinext/cloudflare` `1.0.0-beta.0`
- wrangler `4.105.0`, `compatibility_flags: ["nodejs_compat"]`
- Reproduced on workerd (wrangler dev / production) and Node v26 (undici)
Contributor guide
Research direction
Read packages/vinext/src/server/app-route-handler-runtime.ts and the issue's minimal reproduction first. Verify the route handler request remains a genuine Request in auto mode while static-generation modes retain their behavior, then run the regression tests described in the accompanying PR and confirm re-wrapping works on Node and Workers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- api, backend, cloud
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100