Conversation
ddb62fb to
9a92849
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #102 +/- ##
==========================================
- Coverage 96.19% 96.14% -0.05%
==========================================
Files 8 8
Lines 604 623 +19
Branches 225 233 +8
==========================================
+ Hits 581 599 +18
- Misses 23 24 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@pi0 I believe this PR is ready for review now. All tests passed on my machine locally
|
There was a problem hiding this comment.
Pull request overview
Adds initial HTTP/2 (h2) listener support to the proxy server as a step toward issue #38 (HTTP2/3 support), including middleware updates to handle HTTP/2-specific header semantics and new integration tests.
Changes:
- Introduce
http2?: booleanoption and create an HTTP/2 secure listener (withallowHTTP1) when enabled. - Update incoming/outgoing middleware + core utils to handle HTTP/2 pseudo-headers (
:authority) and remove HTTP/2-incompatible hop-by-hop headers. - Add HTTP/2 integration tests (via
undiciwith h2-enabled agent) and extend stubs/tests forhttpVersionMajor.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/middleware/web-outgoing.test.ts | Adds coverage for :authority redirect rewriting and HTTP/2 connection-header behavior. |
| test/https-proxy.test.ts | Exports helper functions for listening (currently used by new HTTP/2 tests). |
| test/http2-proxy.test.ts | New HTTP/2 listener integration tests (HTTP/2+HTTP/1.1 clients over TLS). |
| test/_stubs.ts | Enhances IncomingMessage stubs with httpVersion/httpVersionMajor; tweaks ProxyServer stub typing. |
| src/types.ts | Adds http2 option and documents SSL usage for HTTP/2 listener. |
| src/server.ts | Implements HTTP/2 secure server creation path; broadens request/response typings to include HTTP/2 compat types. |
| src/middleware/web-outgoing.ts | Removes HTTP/2-incompatible headers (e.g., connection), supports :authority for redirects/status handling. |
| src/middleware/web-incoming.ts | Uses :authority as a fallback for x-forwarded-host. |
| src/middleware/_utils.ts | Extends middleware typing to accept HTTP/2 request/response types (with one remaining generic constraint issue). |
| src/_utils.ts | Filters HTTP/2 pseudo-headers from outgoing request options and uses :authority for host/port derivation. |
| package.json | Adds undici dev dependency for HTTP/2-capable fetch in tests. |
| pnpm-lock.yaml | Locks undici addition. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (4)
src/server.ts:85
- When
http2is enabled, the code always creates a TLS server viahttp2.createSecureServer(...), butsslis still optional inProxyServerOptions. If a user setshttp2: truewithout providing TLS credentials, the server will fail at runtime; consider validatingoptions.ssl(or requiring it in types) and throwing a clear error early.
if (this.options.http2) {
this._server = http2.createSecureServer({ ...this.options.ssl, allowHTTP1: true }, closure);
} else if (this.options.ssl) {
this._server = https.createServer(this.options.ssl, closure);
src/server.ts:43
_webPassesis still typed asProxyMiddleware<http.ServerResponse>[], but whenhttp2is enabled therespassed through the passes can behttp2.Http2ServerResponse. This mismatch is currently papered over with casts in_getPasses/listen, reducing type safety and making it easy for a pass to accidentally use HTTP/1-only APIs. Consider widening the_webPassestype (and the middleware list types) to includeHttp2ServerResponseso incompatibilities are caught by typecheck.
_webPasses: ProxyMiddleware<http.ServerResponse>[] = [...webIncomingMiddleware];
_wsPasses: ProxyMiddleware<net.Socket>[] = [...websocketIncomingMiddleware];
test/http2-proxy.test.ts:25
undiciAgentinstances keep connection pools open; without closing them, the test runner can hang or report open handles. Add a top-levelafterAlltoclose()/destroy()bothhttp1Agentandhttp2Agentonce the suite completes.
const http1Agent = new Agent({
allowH2: false,
connect: {
// Allow to use SSL self signed
rejectUnauthorized: false,
},
});
const http2Agent = new Agent({
allowH2: true,
connect: {
// Allow to use SSL self signed
rejectUnauthorized: false,
},
});
src/middleware/_utils.ts:29
defineProxyMiddleware's generic constraint is stillT extends ServerResponse | Socket, butProxyMiddlewarenow supportsHttp2ServerResponseas well. This makes it impossible to declare a middleware explicitly typed asProxyMiddleware<Http2ServerResponse>(it won't satisfy the constraint) and is inconsistent with the updatedProxyMiddlewaretype. WidendefineProxyMiddleware's constraint to includeHttp2ServerResponse.
export function defineProxyMiddleware<T extends ServerResponse | Socket = ServerResponse>(
m: ProxyMiddleware<T>,
) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds HTTP/2 support across the proxy: types, runtime handling of HTTP/2 pseudo-headers and connection/status behavior, conditional HTTP/2 server creation, and end-to-end tests including HTTP/2 integration and utilities for test listening. Changes
Sequence DiagramsequenceDiagram
participant Client
participant Proxy as HTTP/2 Proxy
participant Target as Backend Server
Client->>Proxy: HTTP/2 request (includes :authority)
Note over Proxy: identify HTTP/2 via httpVersionMajor\nstrip pseudo-headers and copy :authority → host
Proxy->>Target: Outgoing request (HTTP/1.x or HTTP/2 depending on target)
Target-->>Proxy: Response (statusCode, headers)
Note over Proxy: remove connection header for HTTP/2+\nset statusCode (skip statusMessage for HTTP/2)
Proxy-->>Client: HTTP/2 response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
test/_stubs.ts (1)
60-61: Minor type inconsistency in return cast.The return type is
ProxyServer<any, any>but the cast on line 61 uses the unparameterizedProxyServer. This works due to theas unknownintermediate cast but could be slightly cleaner.Optional fix for type consistency
export function stubProxyServer(overrides: Record<string, unknown> = {}): ProxyServer<any, any> { - return overrides as unknown as ProxyServer; + return overrides as unknown as ProxyServer<any, any>; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/_stubs.ts` around lines 60 - 61, The stubProxyServer function returns ProxyServer<any, any> but currently casts to the raw ProxyServer type; update the return cast to match the declared generic parameters—change the final cast to "as unknown as ProxyServer<any, any>" in the stubProxyServer function so the returned type is consistently ProxyServer<any, any>.test/http2-proxy.test.ts (1)
87-91: Consider awaitingsource.close()for proper cleanup.
source.close()(forhttp.Server/https.Server) is asynchronous and accepts an optional callback. While it often works without awaiting, for deterministic test cleanup, consider wrapping it in a Promise or using a callback to ensure the server is fully closed before the test suite ends.♻️ Optional: Await source close for consistent cleanup
afterAll(async () => { // cleans up await new Promise<void>((resolve) => proxy.close(resolve)); - source.close(); + await new Promise<void>((resolve, reject) => + source.close((err) => err ? reject(err) : resolve()) + ); });Also applies to: 161-165
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/http2-proxy.test.ts` around lines 87 - 91, The afterAll cleanup currently calls source.close() without awaiting its completion; change it to await the server close by wrapping source.close in a Promise (or using its callback) so the test waits for full shutdown, similar to how proxy.close(resolve) is awaited; update all occurrences (e.g., the afterAll blocks around source.close at the shown locations) to await the Promise-returning close to ensure deterministic teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 56: The package entry for undici is fine but HTTP/2 is opt-in; when you
create or configure undici's Agent or Client (Agent, Client) for HTTP/2 tests
set the allowH2 option to true (e.g., pass allowH2: true to Agent/Client
constructor or options) so HTTP/2 is actually enabled, and optionally bump the
package version to 7.22.0 if you want the latest patch release.
In `@src/server.ts`:
- Around line 82-88: The server currently calls http2.createSecureServer when
this.options.http2 is true but does not validate that this.options.ssl is
provided; add a runtime check in the server initialization (before calling
http2.createSecureServer) that if this.options.http2 is true and
this.options.ssl is falsy, throw or reject with a clear error explaining that
HTTP/2 requires SSL (referencing ProxyServerOptions in the message), otherwise
proceed to call http2.createSecureServer; ensure the check is placed alongside
the existing branch that inspects this.options.http2 / this.options.ssl so the
error occurs early with a helpful message rather than a cryptic
createSecureServer failure.
In `@test/_utils.ts`:
- Around line 16-27: The proxyListen helper can miss synchronous errors because
proxy.listen(0, "127.0.0.1") is called before attaching the server error
listener; to fix, in proxyListen wrap the call to proxy.listen(...) in a
try/catch and immediately reject on synchronous throw, then grab (proxy as
any)._server and attach server.once("error", reject) and
server.once("listening", ...) as before; keep the existing resolve logic in the
"listening" handler for port resolution.
---
Nitpick comments:
In `@test/_stubs.ts`:
- Around line 60-61: The stubProxyServer function returns ProxyServer<any, any>
but currently casts to the raw ProxyServer type; update the return cast to match
the declared generic parameters—change the final cast to "as unknown as
ProxyServer<any, any>" in the stubProxyServer function so the returned type is
consistently ProxyServer<any, any>.
In `@test/http2-proxy.test.ts`:
- Around line 87-91: The afterAll cleanup currently calls source.close() without
awaiting its completion; change it to await the server close by wrapping
source.close in a Promise (or using its callback) so the test waits for full
shutdown, similar to how proxy.close(resolve) is awaited; update all occurrences
(e.g., the afterAll blocks around source.close at the shown locations) to await
the Promise-returning close to ensure deterministic teardown.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
package.jsonsrc/_utils.tssrc/middleware/_utils.tssrc/middleware/web-incoming.tssrc/middleware/web-outgoing.tssrc/server.tssrc/types.tstest/_stubs.tstest/_utils.tstest/http-proxy.test.tstest/http2-proxy.test.tstest/https-proxy.test.tstest/middleware/web-incoming.test.tstest/middleware/web-outgoing.test.ts
| "sse": "^0.0.8", | ||
| "typescript": "^5.9.3", | ||
| "unbuild": "^3.6.1", | ||
| "undici": "^7.21.0", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if undici 7.21.0 exists and get latest version
echo "=== Checking undici versions ==="
curl -s https://registry.npmjs.org/undici | jq '.dist-tags | {latest, latest_v7: (.["7.21.0"] // "not found")}'
# Get detailed info about 7.21.0
curl -s https://registry.npmjs.org/undici/7.21.0 | jq '{version: .version, name: .name, deprecated: .deprecated}'
# Get latest version info
curl -s https://registry.npmjs.org/undici/latest | jq '{version: .version, name: .name, deprecated: .deprecated}'Repository: unjs/httpxy
Length of output: 386
🏁 Script executed:
#!/bin/bash
# Check for known vulnerabilities in undici
echo "=== Checking for security advisories ==="
curl -s https://registry.npmjs.org/-/npm/v1/security/advisories?package=undici | jq '.advisories // empty'
# Alternative: check npm audit for this package
npm view [email protected] --json | jq '{version: .version, keywords: .keywords[0:5], description: .description}'Repository: unjs/httpxy
Length of output: 282
🌐 Web query:
undici HTTP/2 support Node.js
💡 Result:
Undici (and therefore Node.js’s built-in fetch) is primarily an HTTP/1.1 client, but it does have HTTP/2 client support as an experimental, opt-in feature. [1] [2]
How to use HTTP/2 with undici
Enable it by using an Agent/Client with allowH2: true (HTTP/2 is negotiated via ALPN). The docs also note constraints like “TLS-only” and “h2c is not supported” (so in practice you typically need https:// + a server that negotiates h2). [2]
import { Agent, setGlobalDispatcher, fetch } from 'undici'
setGlobalDispatcher(new Agent({ allowH2: true }))
const res = await fetch('https://example.com/')
console.log(res.status)Node.js built-in fetch and HTTP/2
Node’s built-in fetch() uses the undici version bundled with your Node release, and HTTP/2 is not enabled by default—there’s an open request to support HTTP/2 by default in fetch. [3] [4]
Sources: [1] undici repo README, [2] undici Client API docs (UNPKG) incl. allowH2 + HTTP/2 notes, [3] undici discussion mentioning enabling H2 via allowH2: true for fetch, [4] undici issue requesting HTTP/2-by-default for fetch.
LGTM - undici is suitable for HTTP/2 testing with proper configuration.
The undici 7.21.0 package is stable and has no known security vulnerabilities. However, HTTP/2 support is an experimental, opt-in feature that requires explicit configuration (allowH2: true on the Agent/Client). As a devDependency, it won't affect production bundles. Note that version 7.22.0 is available if you want to stay current with the latest patch.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 56, The package entry for undici is fine but HTTP/2 is
opt-in; when you create or configure undici's Agent or Client (Agent, Client)
for HTTP/2 tests set the allowH2 option to true (e.g., pass allowH2: true to
Agent/Client constructor or options) so HTTP/2 is actually enabled, and
optionally bump the package version to 7.22.0 if you want the latest patch
release.
| export function proxyListen( | ||
| proxy: ReturnType<typeof httpProxy.createProxyServer>, | ||
| ): Promise<number> { | ||
| return new Promise((resolve, reject) => { | ||
| proxy.listen(0, "127.0.0.1"); | ||
| const server = (proxy as any)._server as net.Server; | ||
| server.once("error", reject); | ||
| server.once("listening", () => { | ||
| resolve((server.address() as AddressInfo).port); | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Potential race condition: error listener attached after listen() call.
The error listener on line 22 is attached after proxy.listen() is called on line 20. If the server emits an error synchronously or very quickly, the reject handler may not be registered in time.
Proposed fix to attach error listener before listen
export function proxyListen(
proxy: ReturnType<typeof httpProxy.createProxyServer>,
): Promise<number> {
return new Promise((resolve, reject) => {
- proxy.listen(0, "127.0.0.1");
const server = (proxy as any)._server as net.Server;
+ proxy.listen(0, "127.0.0.1");
+ // Note: _server is created inside listen(), so we must access it after
server.once("error", reject);
server.once("listening", () => {
resolve((server.address() as AddressInfo).port);
});
});
}Actually, since _server is created inside listen(), the current order is necessary. However, this means errors during initial binding could be missed. Consider wrapping in a try-catch or accepting this as a test utility limitation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/_utils.ts` around lines 16 - 27, The proxyListen helper can miss
synchronous errors because proxy.listen(0, "127.0.0.1") is called before
attaching the server error listener; to fix, in proxyListen wrap the call to
proxy.listen(...) in a try/catch and immediately reject on synchronous throw,
then grab (proxy as any)._server and attach server.once("error", reject) and
server.once("listening", ...) as before; keep the existing resolve logic in the
"listening" handler for port resolution.
There was a problem hiding this comment.
very quickly
It is impossible. That's not how JavaScript or the event loop works.
synchronously
What's the point anyway? Throwing in new Promise() will result in an automatic rejection.
There was a problem hiding this comment.
@SukkaW You're absolutely right on both points. I apologize for the confusion.
The event loop guarantees that all synchronous code within the Promise executor completes before any asynchronous events can be processed, so the error and listening listeners will always be attached before those events can fire. Additionally, any synchronous throw within the Promise executor is automatically converted to a rejection by the Promise constructor.
The code is correct as written. Thank you for the clarification!
The HTTP/2 PR accidentally removed proxyRes close and error handlers from web-incoming stream middleware, breaking upstream abort propagation to downstream clients. Restores the handlers and the SSE close test.
Throws a clear error instead of letting http2.createSecureServer fail with a cryptic message when ssl is not provided.
The contributor branch was based on older versions. Restores versions from main and adds undici as new dev dependency.
The comment still said "HTTP 1.0" but the logic now covers both HTTP/1.0 and HTTP/2 (anything that isn't HTTP/1.1).
All supported Node.js versions (16+) have req.socket. The deprecated req.connection and pair fallbacks were unreachable dead code.
Use proper generic type assertions (as Req/Res) instead of as any for the listener callback closure.
Setup logic is already in beforeAll, async on describe is redundant.
Use `req.httpVersion === "1.0" || req.httpVersionMajor >= 2` instead of `!== "1.1"` for clarity. Also restore `https.Server` in `_server` type union.

First step toward #38 with HTTP/2 server/listener support.
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores