denoland/deno · error · NodeTypeError

ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED

ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED

Error message

Cannot set HTTP/2 pseudo-headers

What it means

The same assertValidHeader in http2 compat rejects pseudo-headers — names beginning with ':' such as :status, :method, :path, :authority, :scheme — with ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED. In HTTP/2 these fields are protocol metadata carried outside the header block, so the generic header APIs (setHeader, appendHeader, setTrailer/addTrailers) refuse them.

Source

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

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
    case HTTP2_HEADER_SCHEME: // :scheme
      return true;
    default:

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Use the dedicated APIs: response.statusCode for :status, request.method / request.url for :method/:path
  2. Strip pseudo-headers before forwarding: omit every key starting with ':'
  3. When copying h2 request headers, map :authority to host and drop the rest

Example fix

// before
response.setHeader(":status", 201); // ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED

// after
response.statusCode = 201;
Defensive patterns

Strategy: validation

Validate before calling

function stripPseudoHeaders(headers) {
  return Object.fromEntries(
    Object.entries(headers).filter(([k]) => !k.startsWith(":")),
  );
}
res.writeHead(200, stripPseudoHeaders(incomingHttp2Headers));

Type guard

function isPseudoHeader(name: string): boolean {
  return name.startsWith(":");
}

Try / catch

try {
  res.setHeader(name, value);
} catch (err) {
  if (err.code === "ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED") {
    if (name === ":status") res.statusCode = Number(value);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: response.setHeader(":status", 200); response.addTrailers({ ":status": 200 }); copying an incoming h2 request's raw headers object (which contains :method/:path/:authority) directly onto an outgoing response via setHeader loops.

Common situations: Copy-forward proxies replaying h2 header objects; code ported from the h2 client API (h2.request legitimately takes :method/:path) into server response APIs; generic header middleware that serializes metadata as ':'-prefixed keys.

Related errors


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