denoland/deno · error · NodeTypeError

ERR_HTTP2_INVALID_CONNECTION_HEADERS

ERR_HTTP2_INVALID_CONNECTION_HEADERS

Error message

HTTP/1 Connection specific headers are forbidden: "${key}"

What it means

HTTP/2 forbids HTTP/1.1 connection-control headers: connection, upgrade, http2-settings, keep-alive, proxy-connection, transfer-encoding, and te unless its value is exactly 'trailers' (isIllegalConnectionSpecificHeader, util.ts:595). Any of these in a header set throws ERR_HTTP2_INVALID_CONNECTION_HEADERS (util.ts:872) before HPACK serialization.

Source

Thrown at ext/node/polyfills/internal/http2/util.ts:872

    }
    const flags = ArrayPrototypeIncludes(neverIndex, key)
      ? kNeverIndexFlag
      : kNoHeaderFlags;
    if (key[0] === ":") {
      const err = assertValuePseudoHeader(key);
      if (err !== undefined) {
        throw err;
      }
      value = escapeNgHeaderValueZeroBytes(value);
      pseudoHeaders += `${key}\0${value}\0${flags}`;
      count++;
      return;
    }
    if (!checkIsHttpToken(key)) {
      throw new ERR_INVALID_HTTP_TOKEN("Header name", key);
    }
    if (isIllegalConnectionSpecificHeader(key, value)) {
      throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
    }
    if (isArray) {
      for (let j = 0; j < value.length; ++j) {
        const val = escapeNgHeaderValueZeroBytes(String(value[j]));
        headers += `${key}\0${val}\0${flags}`;
      }
      count += value.length;
      return;
    }
    value = escapeNgHeaderValueZeroBytes(value);
    headers += `${key}\0${value}\0${flags}`;
    count++;
  }

  if (ArrayIsArray(arrayOrMap)) {
    for (let i = 0; i < arrayOrMap.length; i += 2) {
      const key = arrayOrMap[i];
      const value = arrayOrMap[i + 1];

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Strip the forbidden headers before the call: delete connection, upgrade, http2-settings, keep-alive, proxy-connection, transfer-encoding (and te unless 'trailers').
  2. In proxies, translate instead of forwarding: drop hop-by-hop headers per RFC 7230 section 6.1.
  3. Use the dedicated websocket-over-HTTP/2 (RFC 8441 :protocol) API rather than upgrade headers.

Example fix

// before
session.request({ ':method': 'GET', ':path': '/', connection: 'keep-alive', 'transfer-encoding': 'chunked' });

// after
const HOP = ['connection', 'upgrade', 'http2-settings', 'keep-alive', 'proxy-connection', 'transfer-encoding'];
const clean = Object.fromEntries(Object.entries(h).filter(([k]) => !HOP.includes(k.toLowerCase())));
session.request({ ':method': 'GET', ':path': '/', ...clean });
Defensive patterns

Strategy: validation

Validate before calling

const HOP_BY_HOP = new Set(['connection', 'upgrade', 'http2-settings', 'keep-alive', 'proxy-connection', 'transfer-encoding']);
const clean = Object.fromEntries(
  Object.entries(h).filter(([k, v]) =>
    !(HOP_BY_HOP.has(k.toLowerCase()) || (k.toLowerCase() === 'te' && v !== 'trailers'))
  ),
);

Type guard

const isForbiddenConnectionHeader = (k: string, v: string) =>
  ['connection', 'upgrade', 'http2-settings', 'keep-alive', 'proxy-connection', 'transfer-encoding'].includes(k.toLowerCase()) ||
  (k.toLowerCase() === 'te' && v !== 'trailers');

Prevention

When it happens

Trigger: Passing headers such as { connection: 'keep-alive' }, { 'transfer-encoding': 'chunked' }, { upgrade: 'websocket' } or { te: 'gzip' } to http2session.request()/stream.respond(); te: 'trailers' is the sole permitted value.

Common situations: Porting HTTP/1.1 client/server code that set Connection/Transfer-Encoding manually; proxies forwarding upstream HTTP/1.1 headers verbatim into an HTTP/2 request; frameworks that blanket-add connection: keep-alive; misconfigured websocket-over-h2 attempts using the HTTP/1 Upgrade header.

Understand the failure class

Related errors


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