Skip to content

On Node 26.11, reading duplex on the lightweight request throws ERR_INVALID_THIS, so new Request(url, c.req.raw) fails #408

Open

Description

@feketeabsolvingo

Versions

  • @hono/node-server 2.1.5 (also 2.1.4)
  • hono 4.13.12
  • Node 26.11.1 fails (also 26.11.0). Node 26.10.0 works.

What happens

Any code that passes the request c.req.raw from @hono/node-server as a RequestInit now throws on Node 26.11. A common example is rewriting the URL before handing the request to an auth library:

new Request(new URL('/rewritten', c.req.url), c.req.raw)
TypeError [ERR_INVALID_THIS]: Illegal invocation

Every Better Auth endpoint that is routed through this rewrite returns 500.

Minimal reproduction

// package.json: { "type": "module", "dependencies": { "@hono/node-server": "2.1.5", "hono": "4.13.12" } }
import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()
app.all('*', (c) => {
  try {
    const r = new Request(new URL('/rewritten', c.req.url), c.req.raw)
    return c.text(`OK method=${r.method}`)
  } catch (e) {
    return c.text(`FAIL ${e.name} code=${e.code} ${e.message}`, 500)
  }
})

const server = serve({ fetch: app.fetch, port: 0 }, async (info) => {
  const base = `http://127.0.0.1:${info.port}`
  const get = await fetch(`${base}/api/x`)
  const post = await fetch(`${base}/api/x`, { method: 'POST', body: '{"a":1}' })
  console.log(`node ${process.version} | GET: ${get.status} ${await get.text()} | POST: ${post.status} ${await post.text()}`)
  server.close()
})

Output (docker run node:<version>-alpine):

node v26.10.0 | GET: 200 OK method=GET | POST: 200 OK method=POST
node v26.11.1 | GET: 500 FAIL TypeError code=ERR_INVALID_THIS Illegal invocation | POST: 500 FAIL TypeError code=ERR_INVALID_THIS Illegal invocation

To find the failing property, I read each Request property on c.req.raw. On 26.11.1 only one throws:

node v26.10.0 | no property throws
node v26.11.1 | throws: duplex(ERR_INVALID_THIS)

Cause

  • Node 26.11.0 updated undici to 8.11.2. undici 8.11.0 includes use private fields for brand checks nodejs/undici#5792 ("use private fields for brand checks"). Since then, the getters on Request.prototype throw unless this is a real undici Request.
  • The lightweight request in src/request.ts forwards a fixed list of properties to the real request through getRequestCache(): cache, credentials, destination, integrity, mode, redirect, referrer, referrerPolicy and keepalive. duplex is not on that list. Reading duplex therefore falls through to undici's own Request.prototype.duplex getter, which now fails its brand check.
  • The Request constructor reads init.duplex, so passing the lightweight request as init throws.

Suggested fix

Add duplex to the forwarded properties in src/request.ts:

;[
  'cache',
  'credentials',
  'destination',
  'duplex',
  'integrity',
  // ...

Two other fixes are possible, depending on what you want to support: forward every remaining Request.prototype getter so that a future undici getter can't hit the same problem, or return the constant 'half'.

Related

Activity

  1. Ahmad-Mosha commented on Oct 13, 2026

    @Ahmad-Mosha

    I was able to reproduce this on Node 26.11.1 (undici 8.11.2), while 26.10.0 works as expected.

    I also confirmed that Hono's app.mount() returns 500 on 26.11.1, since it passes the lightweight request to new Request(url, request).

    I'll work on the suggested fix (forwarding duplex like the other getters) and add regression tests. I'll open a PR once it's ready.

  2. xandris commented on Oct 14, 2026

    @xandris

    you can but maybe shouldn't monkey patch this while you wait for a fix:

    // HACK: monkey patch the fix for https://github.com/honojs/node-server/issues/408
    let honoHack = false
    
    app.use(
      async (c, next) => {
        if (!honoHack) {
          honoHack = true
          const requestPrototype = Object.getPrototypeOf(c.req.raw)
          const getRequestCache = Object.getOwnPropertySymbols(requestPrototype).find(s =>
            /getRequestCache/.test(s.toString())
          )!
          Object.defineProperty(requestPrototype, 'duplex', {
            get() {
              return this[getRequestCache]().duplex
            }
          })
        }
        await next()
      }
    )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions