denoland/deno · error · NodeTypeError

ERR_INVALID_HTTP_TOKEN

ERR_INVALID_HTTP_TOKEN

Error message

Header name must be a valid HTTP token ["${key}"]

What it means

Non-pseudo header names must be valid HTTP tokens (RFC 7230 token grammar: visible ASCII, no separators/space/CTL). Before a header name is written into the HPACK string, checkIsHttpToken(key) runs and failure throws ERR_INVALID_HTTP_TOKEN with 'Header name' (util.ts:869). This also blocks header injection via CR/LF.

Source

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

        throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key);
      }
      singles.add(key);
    }
    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)) {

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Validate/normalize header names with an HTTP-token regex (e.g. /^[!#$%&'*+.^_|~A-Za-z0-9-]+$/) before building headers.
  2. Reject or sanitize user-supplied names: strip non-token characters or reject outright — do not silently forward.
  3. Keep header names in a constants module instead of interpolating dynamic strings.

Example fix

// before
stream.respond({ ':status': 200, [`x-tenant-${tenantName}`]: '1' }); // tenantName = "acme corp"

// after
const TOKEN = /^[!#$%&'*+.^_|~A-Za-z0-9-]+$/;
const name = `x-tenant-${tenantName}`;
if (!TOKEN.test(name)) throw new Error(`bad header name: ${name}`);
stream.respond({ ':status': 200, [name]: '1' });
Defensive patterns

Strategy: type-guard

Validate before calling

const TOKEN_RE = /^[!#$%&'*+.^_|~A-Za-z0-9-]+$/;
const isValidHeaderName = (name) => typeof name === 'string' && name.length > 0 && TOKEN_RE.test(name);

Type guard

function isHttpToken(name: string): boolean {
  return /^[!#$%&'*+.^_|~A-Za-z0-9-]+$/.test(name);
}

Try / catch

try { stream.respond(h); } catch (e) { if (e.code === 'ERR_INVALID_HTTP_TOKEN') { /* find the name in e.message, sanitize or drop, rebuild h */ } throw e; }

Prevention

When it happens

Trigger: Passing a header name containing a space, comma, colon-adjacent junk, non-ASCII, or control characters, e.g. { 'x custom': 'v' }, { 'café': 'v' }, or user input used directly as a header name in http2session.request()/stream.respond(). Note ':'-prefixed keys take the pseudo-header path, not this one.

Common situations: Using raw user/DB values as header names (x-user-${username}); fat-fingered names with spaces or underscores vs dashes confusion is fine but spaces are not; truncated mojibake strings; security scans probing CRLF injection ("value\r\nX-Evil: 1" used as a name).

Related errors


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