Skip to content

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

Description

@exKAZUu

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)

// 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions