cloudflare / cloudflare/vinext

Route handler request (a Proxy) breaks fetch handler libraries that re-wrap it via new Request() on Workers

Open
#2,557 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.