Fix Node.js 26 `ERR_INTERNAL_ASSERTION` on HTTP/1 responses by davidlehn · Pull Request #85 · digitalbazaar/bedrock-express · GitHub
Skip to content

Fix Node.js 26 ERR_INTERNAL_ASSERTION on HTTP/1 responses - #85

Merged
davidlehn merged 1 commit into
mainfrom
fix-node-26
Oct 21, 2026
Merged

davidlehn merged 1 commit into
mainfrom
fix-node-26

Conversation

@davidlehn

Copy link
Copy Markdown
Member

Fix Node.js 26 ERR_INTERNAL_ASSERTION on HTTP/1 responses

Symptom

On Node.js 26, every HTTP/1 response from a Bedrock app fails with:

Uncaught Error [ERR_INTERNAL_ASSERTION]: This is caused by either a bug in Node.js or incorrect usage of Node.js internals.
    at assert (node:internal/assert:11:11)
    at resOnFinish (node:_http_server:1229:3)
    at ServerResponse.onResponseFinish (node:_http_server:1334:3)
    ...

This was first seen in bedrock-kms-http CI (38–51 failing tests on 26.x, all passing on 22.x/24.x). It reproduces with only @bedrock/server + @bedrock/express, and this repo's own test suite fails on Node 26 too.

The problem first appears in Node.js v26.11.0 (v26.10.0 and all 25.x releases are unaffected). It comes from nodejs/node@15184e8212, "http: reduce per-request allocations in the server" (nodejs/node#65802).

Cause

  • @bedrock/server serves HTTP/1 through http2.createSecureServer({allowHTTP1: true}). Because the server is native HTTP/2, the expressHttp2 hook augments every request with the express request API, HTTP/1 requests included.
  • _http2.augment() returns Object.create(target, descriptors), a new object that has the real request as its prototype. That wrapper becomes req.raw, and _customExpressInit then sets res.req = req to the wrapper.
  • Node < 26.11.0 bound the original req into each response's finish handler (resOnFinish.bind(undefined, req, ...)), so the value of res.req didn't matter.
  • Node 26.11.0 switched to a shared onResponseFinish handler that reads this.req (http: reduce per-request allocations in the server nodejs/node#65802). It then asserts state.incoming[0] === req. The wrapper is not the original request, so the assertion fails.

Instrumenting res.req confirmed it: res.req === original req: false, incoming[0] === original req: true, incoming.length: 1. The replacement happened in _customExpressInit.

Fix

For HTTP/1 requests, define the express request descriptors in place on the raw request (Object.defineProperties) instead of wrapping it, so the request object Node created is the one express sees. Changes:

  • augment() takes a new {inPlace} option.
  • HTTP/1 requests use their own descriptor cache entry (http1Request). The cache filters out properties already present on the first target, and HTTP/1 IncomingMessage and HTTP/2 Http2ServerRequest objects don't have the same properties.
  • HTTP/2 requests are unchanged. Node's HTTP/2 compat response path doesn't have this assertion.
  • hostname, ip and ips are still excluded from the descriptors, so expressifyRequest can keep assigning them. This is also why Object.setPrototypeOf(req.raw, app.request) (the approach used for HTTP/1 responses) doesn't work for requests: those properties are getter-only on the express request prototype.

Tests

  • Added should preserve the node request object. The new /request-identity route checks that the req an express handler receives is the object Node emitted for 'request', and that res.req === req. It fails on every Node version without this fix, so the bug is caught even without Node 26 in the matrix.
Node 24 Node 26
Before 52 passing 1 passing, 15 failing
After 53 passing 53 passing
  • bedrock-kms-http's test suite passes on Node 26 with this change.

Follow-up

  • Release and bump @bedrock/express in bedrock-kms-http (and other consumers testing on Node 26).
  • Possibly report upstream to Node.js. Overwriting res.req worked through v26.10.0 and now triggers an internal assertion, which is arguably a regression from http: reduce per-request allocations in the server nodejs/node#65802, though this fix doesn't depend on that.

When using a native HTTP/2 server with `allowHTTP1`, HTTP/1 requests were
augmented with the express request API by wrapping them with
`Object.create()`. Express then assigned the wrapper to `res.req`. Starting
with Node.js 26.11.0 (nodejs/node#65802), the response finish handler reads
`res.req` and asserts that it is the original request, causing
`ERR_INTERNAL_ASSERTION` on every HTTP/1 response.

Augment HTTP/1 requests in place instead so the request object identity is
preserved. HTTP/2 requests are unchanged.

Add a test that checks express handlers receive the request object that node
emitted. It fails on all node versions without this fix.
@davidlehn
davidlehn requested a review from dlongley October 21, 2026 18:02
@davidlehn
davidlehn merged commit 3d06017 into main Oct 21, 2026
5 checks passed
@davidlehn
davidlehn deleted the fix-node-26 branch October 21, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants