Repository navigation
fix: serve Next.js over Harper's HTTP/2 secure port - #73
Merged
Merged
Conversation
With `http.http2: true`, Harper hands the plugin node:http2 compatibility objects, and Next.js's request handler assumes HTTP/1 ones. Every page returned a 500 (`headers.hasOwnProperty is not a function`, the HTTP/2 headers object has a null prototype) and every compressed response reset the stream (`this._implicitHeader is not a function`, called by the compression middleware Next.js bundles). Adapt the request/response pair per request before calling Next.js. HTTP/1 requests are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HTTP/2 clients send :authority in place of Host, and Node's compatibility layer does not synthesize one, so Next.js saw no host header. Its Server Actions origin check reads host, so an action posted over HTTP/2 would be rejected. Fill host from :authority when the client sent none. Also pin that _implicitHeader does not throw on a stream the client has reset, assert gzip in the HTTP/2 integration test, and fail test helpers cleanly on a connection error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces HTTP/2 compatibility support for Next.js by adapting incoming HTTP/2 request and response objects to match the HTTP/1.1 structures expected by Next.js. It includes new integration and unit tests to verify the compatibility. The feedback suggests two improvements: avoiding the performance overhead of Object.setPrototypeOf by using Object.defineProperty to add hasOwnProperty directly to the headers object, and safely handling cases where the :authority header is an array before assigning it to headers.host.
jjohnson-hdb
marked this pull request as ready for review
October 19, 2026 14:43
Node's HTTP/2 compatibility headers carry `:path`, `:method`, `:authority` and `:scheme`. Next.js converts request headers with `Headers.append()`, which rejects those names, so every App Router route handler (and the middleware adapter) threw `":path" is an invalid header name` over HTTP/2 while pages still rendered. `Http2ServerRequest`'s `url` and `method` are getters over that same headers object, so the pseudo-headers cannot be deleted from it. Shadow `headers` on the request with an HTTP/1-shaped copy instead: an Object-prototype object with the pseudo-headers dropped and `host` filled from `:authority`. That copy also replaces the prototype swap. Add a route handler to the next-16 fixture and request it over HTTP/2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assigning `__proto__` on the Object-prototype copy goes through the prototype setter, which drops the header. Define it as an own property, as Harper's own Node adapter does, and keep the inline comment to the constraint that rules out deleting pseudo-headers in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ethan-Arrowood
requested review from
a team,
cb1kenobi and
dawsontoth
as code owners
October 19, 2026 21:27
|
🎉 This PR is included in version 2.4.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Next.js apps now render when Harper serves them over HTTP/2 (
http.http2: trueon the secure port). Before this, every page returned a 500 and every compressed asset reset its stream, because Next.js's request handler assumes Node's HTTP/1 request/response objects and Harper hands the pluginnode:http2compatibility objects. HTTP/2 is what Chromium needs before it acts on 103 Early Hints, so this was blocking Early Hints for Next.js sites served directly from Harper.The plugin's
serve()insrc/plugin.tsnow runs each request/response pair through the adapter (imported here) before calling Next.js. The adapter is the newadaptHttp2ForNextinsrc/http2Compat.ts. For HTTP/2 only, it does three things:Object.prototype, because Next.js callsheaders.hasOwnProperty(). It drops the:path,:method,:authorityand:schemepseudo-headers, because Next.js passes every header name toHeaders.append(), which rejects them, so route handlers threw. It shadows theheadersgetter instead of editing the original, becauseurlandmethodread that object. A__proto__header is kept as an own property;hostfrom:authoritywhen the client sent nohost, because Next.js's Server Actions origin check readshost;_implicitHeader()for thecompressionmiddleware Next.js bundles. It writes the head only while headers are unsent.HTTP/1 requests return before any of this runs.
For the human reviewer
Requirement: does this belong in the plugin? Next.js doesn't support
node:http2compatibility objects in a custom server. Harper usescreateSecureServerwithallowHTTP1whenhttp.http2is on. The plugin'sserve()is the one place the two meet. Two alternatives were considered:Http2ServerRequest/Http2ServerResponseprototypes globally, which would change everynode:http2user in the process.If Harper later normalizes these objects, this adapter becomes a harmless no-op. Declining the fix leaves a compatibility gap for Next.js sites on
http.http2: true.The response is patched in place; the request gets a headers copy.
_implicitHeaderis set on the response instance and Harper's_nodeRequestidentity is kept. The headers are copied because deleting the pseudo-headers from the original breaksreq.urlandreq.method. The trade-off is one object allocation per HTTP/2 request, and Harper'sRequestHeaders, which wraps the original object, does not see the filledhost. HTTP/1 pays nothing.When
hostand:authoritydisagree, the client'shostwins. The adapter only fillshostwhen it's absent. The alternatives are to prefer:authority(as Node'sreq.authoritydoes) or to reject the mismatch, which RFC 9113 §8.3.1 allows. The risk is an intermediary that routes on:authoritypassing a differenthostinto Next.js's redirects and absolute URLs. Each option is a one-line change.Only the
next-16fixture runs over HTTP/2 end to end. Next.js 14 and 15 bundle the samecompressionmiddleware, and the adapter is version-independent. Covering them means adding anhttpoption to each of those fixtures.One review finding was judged unfounded and left as it is: that
instanceofcould fail across VM realms. Node built-ins are shared across the process, and the integration test passes on the default loader. The earliersetPrototypeOfquestion no longer applies, because the copy replaces the prototype swap.Verification
Unit,
node --test dist/http2Compat.test.js: 10/10 pass. The tests in the newsrc/http2Compat.test.tsuse a realnode:http2server and Next.js 16's own bundledcompression. They show that:hasOwnProperty;urlandmethodstill work, and Next.js 16's ownfromNodeOutgoingHttpHeadersaccepts the result;hostis filled from:authority, and ahostthe client sent is kept;__proto__header is copied as a header, not a prototype;_implicitHeaderdoesn't throw after a client reset;With the adapter turned into a no-op, the positive tests fail.
Integration, new
integrationTests/next-16-http2.pw.ts. It starts Harper withhttp: { http2: true, mtls: false }, which binds the secure port. With the fix, all four tests pass: the home page returns 200 gzipped over HTTP/2, a hashed static chunk returns 200, a route handler returns its JSON over HTTP/2, and HTTP/1.1 on the same secure port still returns 200. On base, the page and asset tests fail withProtocol error(the stream reset) and the HTTP/1.1 control passes. With the adapter as first pushed (c041944), the route-handler test fails with a 500.integrationTests/fixture.tsgains thehttpoption and passes it into Harper's config.Real app. The a homepage build (Next.js 16, webpack) was served through
node:http2with this adapter. The home page, an RSC request,guest-home, a JS chunk and a CSS file all return the same status over HTTP/2 as over HTTP/1.1, gzipped, with no handler errors. On Harper Fabric stage withhttp.http2: true, the same build returned 500 orINTERNAL_ERRORbefore the fix.Full suites.
npm test: 156/156 pass at1f387cd.npm run test:integration: 46 passed at1f387cd, with freshly installed fixtures.cluster.pw.tsdid not run: its Docker rig couldn't bind port 29925, which an unrelated local container already holds. That's an environment conflict, not a test failure.Review of the fix commits. Codex and Gemini reviewed
cb0026aand1f387cdbefore push, with a Harper-domain adjudication, over two rounds against this branch. It converged with one disputed nit: the fixture route stays JavaScript, like the rest offixtures/next-16. The footers below record the original review atc041944.Related PRs: #62 overlaps (edits the same request handler in
src/plugin.ts; whichever lands second needs a rebase)Complexity: medium
🤖 Generated by Claude Opus 5.5; posted via @Ethan-Arrowood.
Review-Coverage: authored=claude; ran=codex,gemini; adjudicated=domain; declined=cursor-grok,cursor-composer; rounds=2; full=2 @ c041944
Human-Review-Need: 3 (decisions: host-vs-authority-precedence, per-instance-vs-prototype-shim, compensate-in-plugin-vs-harper) @ c041944