Bug Description
When client.upgrade({ path: '/', protocol: 'websocket' }) runs over HTTP/2 and the server replies to the extended CONNECT request with a non-200 status such as :status = 404, Undici crashes the process with an uncaught AssertionError [ERR_ASSERTION] instead of rejecting client.upgrade().
Reproducible By
Run for the root of the repo
import { readFileSync } from "node:fs";
import { once } from "node:events";
import { createSecureServer } from "node:http2";
import { Client } from "./index.js";
const server = createSecureServer({
key: readFileSync("./test/fixtures/key.pem"),
cert: readFileSync("./test/fixtures/cert.pem"),
settings: { enableConnectProtocol: true },
});
server.on("stream", (stream, headers) => {
const isWebSocketConnect =
headers[":method"] === "CONNECT" && headers[":protocol"] === "websocket";
if (isWebSocketConnect) {
stream.respond({ ":status": 404 });
stream.end("not found");
return;
}
stream.respond({ ":status": 200 });
stream.end();
});
await once(server.listen(0), "listening");
const { port } = server.address();
const client = new Client(`https://localhost:${port}`, {
connect: { rejectUnauthorized: false },
allowH2: true,
});
const closeServer = () => new Promise((resolve) => server.close(resolve));
try {
await client.upgrade({ path: "/", protocol: "websocket" });
console.log("upgrade unexpectedly resolved");
} catch (err) {
console.log("upgrade rejected:", err?.name, err?.message);
} finally {
await client.close().catch(() => {});
await closeServer();
}
Observe that the process exits with AssertionError [ERR_ASSERTION] instead of rejecting the promise
Expected Behavior
A non-200 HTTP/2 response to client.upgrade() should reject the request with a regular request error, not abort the process. Something equivalent to SocketError('bad upgrade') would match the current upgrade error handling more closely than an uncaught assertion.
Logs & Screenshots
node:internal/assert/utils:77
throw err;
^
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
assert(socket[kHTTP2Stream] === true ? statusCode === 200 : statusCode === 101)
at UpgradeHandler.onRequestUpgrade (/Users/trivikram/workspace/undici/lib/api/api-upgrade.js:54:5)
at Request.onRequestUpgrade (/Users/trivikram/workspace/undici/lib/core/request.js:382:45)
at ClientHttp2Stream.<anonymous> (/Users/trivikram/workspace/undici/lib/dispatcher/client-h2.js:537:17)
at Object.onceWrapper (node:events:623:26)
at ClientHttp2Stream.emit (node:events:508:28)
at emit (node:internal/http2/core:343:3)
at process.processTicksAndRejections (node:internal/process/task_queues:93:22) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
Node.js v24.14.1
Environment
macOS 26.4.1
Node v24.14.1
undici v8.1.0
Bug Description
When
client.upgrade({ path: '/', protocol: 'websocket' })runs over HTTP/2 and the server replies to the extended CONNECT request with a non-200status such as:status = 404, Undici crashes the process with an uncaughtAssertionError [ERR_ASSERTION]instead of rejectingclient.upgrade().Reproducible By
Run for the root of the repo
Observe that the process exits with
AssertionError [ERR_ASSERTION]instead of rejecting the promiseExpected Behavior
A non-
200HTTP/2 response toclient.upgrade()should reject the request with a regular request error, not abort the process. Something equivalent toSocketError('bad upgrade')would match the current upgrade error handling more closely than an uncaught assertion.Logs & Screenshots
Environment
macOS 26.4.1
Node v24.14.1
undici v8.1.0