Repository navigation
On Node 26.11, reading duplex on the lightweight request throws ERR_INVALID_THIS, so new Request(url, c.req.raw) fails #408
Copy link
Copy link
Open
Description
Versions
@hono/node-server2.1.5 (also 2.1.4)hono4.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.prototypethrow unlessthisis a real undiciRequest. - The lightweight request in
src/request.tsforwards a fixed list of properties to the real request throughgetRequestCache():cache,credentials,destination,integrity,mode,redirect,referrer,referrerPolicyandkeepalive.duplexis not on that list. Readingduplextherefore falls through to undici's ownRequest.prototype.duplexgetter, which now fails its brand check. - The
Requestconstructor readsinit.duplex, so passing the lightweight request asinitthrows.
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
- bodyLimit answers 500 for a body without Content-Length when node-server runs with overrideGlobalObjects: false hono#5538: the same kind of failure in
bodyLimit(new Request(c.req.raw, init)) withoverrideGlobalObjects: false. - use private fields for brand checks nodejs/undici#5792: the brand-check change.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
Ahmad-Mosha commentedon Oct 13, 2026
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 tonew Request(url, request).I'll work on the suggested fix (forwarding
duplexlike the other getters) and add regression tests. I'll open a PR once it's ready.xandris commentedon Oct 14, 2026
you can but maybe shouldn't monkey patch this while you wait for a fix: