denoland/deno · error · TypeError

Invalid Header: 'upgrade' header must contain 'websocket'

Error message

Invalid Header: 'upgrade' header must contain 'websocket'

What it means

Deno.upgradeWebSocket(request) validates the WebSocket handshake the client sent. The request's 'upgrade' header must contain the token 'websocket' (case-insensitive, comma-separated list, matched by websocketCvf). A missing header, or values like 'h2c', 'websocketx', or a typo, fail this check.

Source

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

const loadWebSocket = core.createLazyLoader(
  "ext:deno_websocket/01_websocket.js",
);

const _ws = Symbol("[[associated_ws]]");

const websocketCvf = buildCaseInsensitiveCommaValueFinder("websocket");
const upgradeCvf = buildCaseInsensitiveCommaValueFinder("upgrade");

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",
    );

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Only call upgradeWebSocket when the request is a real handshake: check req.headers.get('upgrade')?.toLowerCase().includes('websocket') first.
  2. Fix the proxy: nginx needs proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade".
  3. Connect with a real WebSocket client (new WebSocket('ws://...') or ws:// in browser) instead of plain HTTP.
  4. Route WebSocket handshakes to a dedicated handler so ordinary requests never reach the upgrade code.

Example fix

// before
function handler(req: Request) {
  const { socket, response } = Deno.upgradeWebSocket(req); // crashes for plain GET
  ...
}

// after
function handler(req: Request) {
  if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
    return new Response("expected websocket", { status: 400 });
  }
  const { socket, response } = Deno.upgradeWebSocket(req);
  return response;
}
Defensive patterns

Strategy: validation

Validate before calling

function isWebSocketHandshake(req: Request): boolean {
  const up = (req.headers.get("upgrade") ?? "").toLowerCase();
  return up.split(",").map((s) => s.trim()).includes("websocket");
}

Type guard

function isWsUpgradeRequest(req: Request): boolean {
  const tok = (h: string | null) => (h ?? "").toLowerCase().split(",").map((s) => s.trim());
  return tok(req.headers.get("upgrade")).includes("websocket") &&
    tok(req.headers.get("connection")).includes("upgrade") &&
    req.headers.has("sec-websocket-key");
}

Try / catch

try { return Deno.upgradeWebSocket(req).response; } catch (e) { if (e instanceof TypeError && e.message.includes("'upgrade' header")) { return new Response("websocket handshake required", { status: 400 }); } throw e; }

Prevention

When it happens

Trigger: Calling Deno.upgradeWebSocket(req) inside a plain HTTP GET handler (no upgrade requested); client sent Upgrade: h2c or a custom protocol; a proxy (nginx, ALB) stripped or rewrote the Upgrade header; testing the route with curl without the upgrade headers.

Common situations: Reverse proxies not forwarding Upgrade/Connection headers (proxy_setupgrade upgrade missing); load balancers terminating WebSocket upgrades; development probes (curl/health checks) hitting the ws route; shared handlers that run for both ws and non-ws requests.

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/c8662196ef2cb991. Report an issue: GitHub.