Skip to content

H2 websocket upgrade crashes process on non-200 response instead of rejecting client.upgrade() #5063

@trivikr

Description

@trivikr

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions