Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"sse": "^0.0.8",
"typescript": "^5.9.3",
"unbuild": "^3.6.1",
"undici": "^7.21.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 undici@7.21.0 --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.

"vitest": "^4.0.18",
"ws": "^8.19.0"
},
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 64 additions & 9 deletions src/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import httpNative from "node:http";
import httpsNative from "node:https";
import net from "node:net";
import type tls from "node:tls";
import type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from "./types.ts";
import type { Http2ServerRequest } from "node:http2";

const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i;

Expand All @@ -11,6 +11,15 @@ const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i;
*/
export const isSSL = /^https|wss/;

/**
* Node.js HTTP/2 accepts pseudo headers and it may conflict
* with request options.
*
* Let's just blacklist those potential conflicting pseudo
* headers.
*/
const HTTP2_HEADER_BLACKLIST = [":method", ":path", ":scheme", ":authority"];

/**
* Copies the right headers from `options` and `req` to
* `outgoing` which is then used to fire the proxied
Expand Down Expand Up @@ -38,7 +47,7 @@ export function setupOutgoing(
ca?: string;
method?: string;
},
req: httpNative.IncomingMessage,
req: httpNative.IncomingMessage | Http2ServerRequest,
forward?: "forward" | "target",
): httpNative.RequestOptions | httpsNative.RequestOptions {
outgoing.port =
Expand All @@ -64,10 +73,23 @@ export function setupOutgoing(
outgoing.method = options.method || req.method;
outgoing.headers = { ...req.headers };

// before clean up HTTP/2 blacklist header, we might wanna override host first
if (req.headers?.[":authority"]) {
outgoing.headers.host = req.headers[":authority"] as string;
}
// host override must happen before composing/merging the final outgoing headers

if (options.headers) {
outgoing.headers = { ...outgoing.headers, ...options.headers };
}

if (req.httpVersionMajor > 1) {
// ignore potential conflicting HTTP/2 pseudo headers
for (const header of HTTP2_HEADER_BLACKLIST) {
delete outgoing.headers[header];
}
}

if (options.auth) {
outgoing.auth = options.auth;
}
Expand Down Expand Up @@ -181,8 +203,9 @@ export function setupSocket(socket: net.Socket): net.Socket {
*
* @api private
*/
export function getPort(req: httpNative.IncomingMessage): string {
const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : "";
export function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): string {
const hostHeader = (req.headers[":authority"] as string | undefined) || req.headers.host;
const res = hostHeader ? hostHeader.match(/:(\d+)/) : "";
if (res) {
return res[1]!;
}
Expand All @@ -198,11 +221,43 @@ export function getPort(req: httpNative.IncomingMessage): string {
*
* @api private
*/
export function hasEncryptedConnection(req: httpNative.IncomingMessage): boolean {
return Boolean(
// req.connection.pair probably does not exist anymore
(req.connection as tls.TLSSocket).encrypted || (req.connection as any).pair,
);
export function hasEncryptedConnection(
req: httpNative.IncomingMessage | Http2ServerRequest,
): boolean {
// Since Node.js v16 we now have req.socket
if ("socket" in req) {
/* v8 ignore start */

// encrypted is only present in TLS sockets, not plain net sockets
if ("encrypted" in req.socket) {
return req.socket.encrypted;
}

// "pair" is deprecated and is not typed by @types/node, but it actually hasn't been removed yet
// we can still fall back to "pair" for backward compatibility, but normally not reachable
if ("pair" in req.socket) {
return !!req.socket.pair;
}
/* v8 ignore stop */
}

if ("connection" in req) {
/* v8 ignore start */

// encrypted is only present in TLS sockets, not plain net sockets
if ("encrypted" in req.connection) {
return req.connection.encrypted;
}

// "pair" is deprecated and is not typed by @types/node, but it actually hasn't been removed yet
// we can still fall back to "pair" for backward compatibility, but normally not reachable
if ("pair" in req.connection) {
return !!req.connection.pair;
}
/* v8 ignore stop */
}

return false;
}

/**
Expand Down
17 changes: 9 additions & 8 deletions src/middleware/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import type { Socket } from "node:net";
import type { ProxyServer } from "../server.ts";
import type { ProxyServerOptions, ProxyTargetDetailed } from "../types.ts";
import type { Http2ServerRequest, Http2ServerResponse } from "node:http2";

export type ResOfType<T extends "web" | "ws"> = T extends "ws"
? T extends "web"
? ServerResponse | Socket
? ServerResponse | Http2ServerResponse | Socket
: Socket
: T extends "web"
? ServerResponse
? ServerResponse | Http2ServerResponse
: never;

export type ProxyMiddleware<T extends ServerResponse | Socket> = (
req: IncomingMessage,
export type ProxyMiddleware<T extends ServerResponse | Http2ServerResponse | Socket> = (
req: IncomingMessage | Http2ServerRequest,
res: T,
opts: ProxyServerOptions & {
target: URL | ProxyTargetDetailed;
forward: URL;
},
server: ProxyServer,
server: ProxyServer<IncomingMessage | Http2ServerRequest, ServerResponse | Http2ServerResponse>,
head?: Buffer,
callback?: (err: any, req: IncomingMessage, socket: T, url?: any) => void,
callback?: (err: any, req: IncomingMessage | Http2ServerRequest, socket: T, url?: any) => void,
) => void | true;

export function defineProxyMiddleware<T extends ServerResponse | Socket = ServerResponse>(
Expand All @@ -30,8 +31,8 @@ export function defineProxyMiddleware<T extends ServerResponse | Socket = Server
}

export type ProxyOutgoingMiddleware = (
req: IncomingMessage,
res: ServerResponse,
req: IncomingMessage | Http2ServerRequest,
res: ServerResponse | Http2ServerResponse,
proxyRes: IncomingMessage,
opts: ProxyServerOptions & {
target: URL | ProxyTargetDetailed;
Expand Down
3 changes: 2 additions & 1 deletion src/middleware/web-incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export const XHeaders = defineProxyMiddleware((req, res, options) => {
values[header];
}

req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers.host || "";
req.headers["x-forwarded-host"] =
req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers.host || "";
});

/**
Expand Down
33 changes: 23 additions & 10 deletions src/middleware/web-outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ const redirectRegex = /^201|30([1278])$/;
* If is a HTTP 1.0 request, remove chunk headers
*/
export const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) => {
if (req.httpVersion === "1.0") {
// HTTP/1.0 and HTTP/2 do not have transfer-encoding: chunked
if (req.httpVersion !== "1.1") {
delete proxyRes.headers["transfer-encoding"];
}
});

/**
* If is a HTTP 1.0 request, set the correct connection header
* or if connection header not present, then use `keep-alive`
*
* If is a HTTP/2 request, remove connection header no matter what,
* this avoids sending connection header to the underlying http2 client
*/
export const setConnection = defineProxyOutgoingMiddleware((req, res, proxyRes) => {
if (req.httpVersion === "1.0") {
proxyRes.headers.connection = req.headers.connection || "close";
} else if (req.httpVersion !== "2.0" && !proxyRes.headers.connection) {
} else if (req.httpVersionMajor < 2 && !proxyRes.headers.connection) {
proxyRes.headers.connection = req.headers.connection || "keep-alive";
} else if (req.httpVersionMajor >= 2) {
delete proxyRes.headers.connection;
}
});

Expand All @@ -42,8 +48,12 @@ export const setRedirectHostRewrite = defineProxyOutgoingMiddleware(

if (options.hostRewrite) {
u.host = options.hostRewrite;
} else if (options.autoRewrite && req.headers.host) {
u.host = req.headers.host;
} else if (options.autoRewrite) {
if (req.headers[":authority"]) {
u.host = req.headers[":authority"] as string;
} else if (req.headers.host) {
u.host = req.headers.host;
}
}
if (options.protocolRewrite) {
u.protocol = options.protocolRewrite;
Expand Down Expand Up @@ -116,13 +126,16 @@ export const writeHeaders = defineProxyOutgoingMiddleware((req, res, proxyRes, o
*/
export const writeStatusCode = defineProxyOutgoingMiddleware((req, res, proxyRes) => {
// From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers])
if (proxyRes.statusMessage) {
// @ts-expect-error
res.statusCode = proxyRes.statusCode;

// @ts-expect-error
res.statusCode = proxyRes.statusCode;

if (
proxyRes.statusMessage &&
// Only HTTP/1.0 and HTTP/1.1 support statusMessage
req.httpVersionMajor < 2
) {
res.statusMessage = proxyRes.statusMessage;
} else {
// @ts-expect-error
res.statusCode = proxyRes.statusCode;
}
});

Expand Down
Loading