denoland/deno · error · RangeError

ERR_HTTP2_STATUS_INVALID

ERR_HTTP2_STATUS_INVALID

Error message

Invalid status code: ${statusCode}

What it means

validatePreparedResponseHeaders enforces that the :status on an HTTP/2 response is within 200-599, deliberately stricter than HTTP/1's 100-999, because HTTP/2 started fresh on spec compliance (the comment in the polyfill says exactly this). Statuses outside that range — including 1xx informational codes and anything >= 600 like custom 599s — throw ERR_HTTP2_STATUS_INVALID when passed to stream.respond() / respondWithFD().

Source

Thrown at ext/node/polyfills/http2.ts:2780

  if (!isDateSet && (options.sendDate == null || options.sendDate)) {
    ArrayPrototypePush(headers, HTTP2_HEADER_DATE, utcDate());
  }

  validatePreparedResponseHeaders(headers, statusCode);

  return { headers, statusCode };
}

function validatePreparedResponseHeaders(headers, statusCode) {
  // This is intentionally stricter than the HTTP/1 implementation, which
  // allows values between 100 and 999 (inclusive) in order to allow for
  // backwards compatibility with non-spec compliant code. With HTTP/2,
  // we have the opportunity to start fresh with stricter spec compliance.
  // This will have an impact on the compatibility layer for anyone using
  // non-standard, non-compliant status codes.
  if (statusCode < 200 || statusCode > 599) {
    throw new ERR_HTTP2_STATUS_INVALID(statusCode);
  }

  const neverIndex = headers[kSensitiveHeaders];
  if (neverIndex !== undefined && !ArrayIsArray(neverIndex)) {
    throw new ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", neverIndex);
  }
}

function tryClose(fd) {
  fs.close(fd, (err) => {
    if (err) throw err;
  });
}

function processRespondWithFD(
  self,
  fd,
  headers,

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Clamp or reject before responding: if (status < 200 || status > 599) map to a valid code (e.g. 501 or 500)
  2. For upgrades, switch to RFC 8441 extended CONNECT (enableConnectProtocol) instead of forwarding 101
  3. Handle 103 Early Hints via a separate informational mechanism, never through respond() on the same stream

Example fix

// before
stream.respond({ ':status': 101 }); // throws ERR_HTTP2_STATUS_INVALID

// after
const status = upstreamStatus >= 200 && upstreamStatus <= 599
  ? upstreamStatus
  : 501;
stream.respond({ ':status': status });
Defensive patterns

Strategy: validation

Validate before calling

const h2Status = (code: number): number =>
  Number.isInteger(code) && code >= 200 && code <= 599 ? code : 501;
stream.respond({ ':status': h2Status(upstreamStatus) });

Type guard

const isH2ValidStatus = (code: unknown): code is number =>
  Number.isInteger(code) && (code as number) >= 200 && (code as number) <= 599;

Prevention

When it happens

Trigger: stream.respond({ ':status': 101 }) when proxying a WebSocket upgrade; :status 100/103 informational responses forwarded from an origin; custom non-standard codes like 599 or 999 copied from HTTP/1 proxy behavior.

Common situations: HTTP/1-to-HTTP/2 proxies forwarding 101 Switching Protocols (HTTP/2 forbids upgrade; extended CONNECT is the replacement); apps using 599 as an internal error sentinel; Early Hints (103) implementations that try to send them as a normal respond() on the same stream.

Related errors


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