denoland/deno · error · TypeError

Invalid Header: 'connection' header must contain 'Upgrade'

Error message

Invalid Header: 'connection' header must contain 'Upgrade'

What it means

Second handshake check in `Deno.upgradeWebSocket`: the `Connection` header's comma-separated values must include the token `Upgrade` (case-insensitive), because RFC 6455 requires the client to opt in with `Connection: Upgrade`. A missing or rewritten header throws a TypeError before the socket is created.

Solutions

  1. Configure the proxy to pass the token: `proxy_set_header Connection "upgrade"`
  2. Ensure clients use a real WebSocket implementation (browser WebSocket API or a ws library)
  3. Check both `upgrade` and `connection` headers before calling `upgradeWebSocket`, and answer 400 otherwise

Example fix

// before
const ws = Deno.upgradeWebSocket(request);

// after
const conn = request.headers.get("connection") ?? "";
if (!/upgrade/i.test(conn)) {
  return new Response("Connection: Upgrade required", { status: 400 });
}
const ws = Deno.upgradeWebSocket(request);
Defensive patterns

Strategy: validation

Validate before calling

const conn = request.headers.get("connection") ?? "";
const hasUpgradeToken = conn.split(",").some((v) => v.trim().toLowerCase() === "upgrade");
if (!hasUpgradeToken) return new Response("Connection: Upgrade required", { status: 400 });
const ws = Deno.upgradeWebSocket(request);

Type guard

const connectionHasUpgrade = (req: Request): boolean =>
  (req.headers.get("connection") ?? "")
    .split(",")
    .some((v) => v.trim().toLowerCase() === "upgrade");

Try / catch

try {
  return Deno.upgradeWebSocket(request);
} catch (err) {
  if (err instanceof TypeError && err.message.includes("connection")) {
    return new Response("Bad handshake", { status: 400 });
  }
  throw err;
}

Prevention

When it happens

Trigger: Intermediaries that rewrite `Connection: keep-alive, Upgrade` to plain `keep-alive`; HTTP/2 requests (which forbid the Connection header entirely); hand-rolled clients sending only `Upgrade: websocket` without the Connection token.

Common situations: Reverse proxies and WAFs normalizing hop-by-hop headers; API gateways terminating WebSocket-incompatible frontends; testing with HTTP/2-enabled tooling.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/f0897b0aeef6606e. Report an issue: GitHub.

Appendix: source

Thrown at ext/http/02_websocket.ts:56

function upgradeWebSocket(request, options = { __proto__: null }) {
  const inner = toInnerRequest(request);
  if (inner._wantsUpgrade) {
    inner._throwIfUpgraded();
  }
  const upgrade = request.headers.get("upgrade");
  const upgradeHasWebSocketOption = upgrade !== null &&
    websocketCvf(upgrade);
  if (!upgradeHasWebSocketOption) {
    throw new TypeError(
      "Invalid Header: 'upgrade' header must contain 'websocket'",
    );
  }

  const connection = request.headers.get("connection");
  const connectionHasUpgradeOption = connection !== null &&
    upgradeCvf(connection);
  if (!connectionHasUpgradeOption) {
    throw new TypeError(
      "Invalid Header: 'connection' header must contain 'Upgrade'",
    );
  }

  const websocketKey = request.headers.get("sec-websocket-key");
  if (websocketKey === null) {
    throw new TypeError(
      "Invalid Header: 'sec-websocket-key' header must be set",
    );
  }

  const accept = op_http_websocket_accept_header(websocketKey);

  const r = newInnerResponse(101);
  r.headerList = [
    ["upgrade", "websocket"],
    ["connection", "Upgrade"],
    ["sec-websocket-accept", accept],

View on GitHub (pinned to 9ad36f7a2c)