denoland/deno · error · NodeTypeError

ERR_INVALID_HTTP_TOKEN

ERR_INVALID_HTTP_TOKEN

Error message

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

What it means

http2 compat validates every header passing through Http2ServerResponse.setHeader/appendHeader/setTrailer/writeHead via assertValidHeader. A header name that is empty, not a string, or contains a space is rejected with ERR_INVALID_HTTP_TOKEN because HTTP header names must be RFC 7230 tokens (alphanumerics and !#$%&'*+-.^_`|~).

Source

Thrown at ext/node/polyfills/internal/http2/compat.js:99

const kRawTrailers = Symbol("rawTrailers");
const kSetHeader = Symbol("setHeader");
const kAppendHeader = Symbol("appendHeader");
const kAborted = Symbol("aborted");

let statusMessageWarned = false;
let statusConnectionHeaderWarned = false;

// Defines and implements an API compatibility layer on top of the core
// HTTP/2 implementation, intended to provide an interface that is as
// close as possible to the current require('http') API

const assertValidHeader = hideStackFrames((name, value) => {
  if (
    name === "" ||
    typeof name !== "string" ||
    StringPrototypeIncludes(name, " ")
  ) {
    throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError("Header name", name);
  }
  if (isPseudoHeader(name)) {
    throw new ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED.HideStackFramesError();
  }
  if (value === undefined || value === null) {
    throw new ERR_HTTP2_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
  }
  if (!isConnectionHeaderAllowed(name, value)) {
    connectionHeaderMessageWarn();
  }
});

function isPseudoHeader(name) {
  switch (name) {
    case HTTP2_HEADER_STATUS: // :status
    case HTTP2_HEADER_METHOD: // :method
    case HTTP2_HEADER_PATH: // :path
    case HTTP2_HEADER_AUTHORITY: // :authority

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Sanitize dynamic names to token characters: /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/ — replace or drop invalid ones
  2. Map user-controlled values onto a fixed allow-list of header names instead of using them as names
  3. Coerce keys with String(key).trim() and skip empty results
  4. If the data is dirty, put it in the header value (encodeURIComponent) and use a constant name

Example fix

// before
const name = `trace-${userTag}`; // userTag = "my tag"
response.setHeader(name, "1"); // ERR_INVALID_HTTP_TOKEN

// after
const name = `trace-${String(userTag).trim().replace(/[^!#$%&'*+\-.^_`|~0-9A-Za-z]/g, "-")}`;
if (name) response.setHeader(name, "1");
Defensive patterns

Strategy: validation

Validate before calling

const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
function safeHeaderName(name) {
  const s = String(name).trim();
  return TOKEN_RE.test(s) ? s : null;
}
const safe = safeHeaderName(userSuppliedName);
if (safe) res.setHeader(safe, value);

Type guard

function isValidHeaderName(name: unknown): name is string {
  return typeof name === "string" && name.length > 0 &&
    /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
}

Try / catch

try {
  res.setHeader(name, value);
} catch (err) {
  if (err.code === "ERR_INVALID_HTTP_TOKEN") return; // skip bad name, keep serving
  throw err;
}

Prevention

When it happens

Trigger: response.setHeader("", value); setHeader("content type", v) with a space in the name; passing a non-string key (number, symbol from Object.keys of a Map); header names built from user input containing spaces, unicode, or control characters.

Common situations: Proxies/gateways forwarding arbitrary client-supplied header names; names derived from filenames, IDs, or locale strings; header-injection payloads ('x-inject\r\nHost:...') reaching the validator; code ported from http1 stacks that were lenient.

Related errors


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