denoland/deno · error · Error

ERR_HTTP2_PAYLOAD_FORBIDDEN

ERR_HTTP2_PAYLOAD_FORBIDDEN

Error message

Responses with ${statusCode} status must not have a payload

What it means

respondWithFD() refuses to send file data when the effective status is 204 (No Content), 205 (Reset Content), 304 (Not Modified), or the request was a HEAD request — HTTP semantics forbid a body/DATA frames in those cases. The check runs after prepareResponseHeadersObject computes the status, so a cached 304 path still trips it.

Source

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

    debugStreamObj(this, "initiating response from fd");
    this[kUpdateTimer]();
    this.ownsFd = false;

    const {
      headers,
      statusCode,
    } = prepareResponseHeadersObject(headersParam, options);

    setOtelServerStatus(this, statusCode);

    // Payload/DATA frames are not permitted in these cases
    if (
      statusCode === HTTP_STATUS_NO_CONTENT ||
      statusCode === HTTP_STATUS_RESET_CONTENT ||
      statusCode === HTTP_STATUS_NOT_MODIFIED ||
      this.headRequest
    ) {
      throw new ERR_HTTP2_PAYLOAD_FORBIDDEN(statusCode);
    }

    if (options.statCheck !== undefined) {
      fs.fstat(
        fd,
        FunctionPrototypeBind(
          doSendFD,
          this,
          session,
          options,
          fd,
          headers,
          streamOptions,
        ),
      );
      return;
    }

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. For 204/205/304 call stream.respond(headers, { endStream: true }) instead — no payload API at all
  2. Skip the body entirely for HEAD requests: respond with endStream and never call respondWithFD
  3. Branch on req.method and status before choosing respondWithFD vs respond
  4. Revalidation hits need only headers: emit them and end the stream

Example fix

// before
if (notModified) {
  stream.respondWithFD(fd, { ":status": 304, etag });
}

// after
if (notModified) {
  stream.respond({ ":status": 304, etag }, { endStream: true });
  return;
}
Defensive patterns

Strategy: validation

Validate before calling

const bodyForbidden =
  statusCode === 204 || statusCode === 205 || statusCode === 304 ||
  stream.headRequest; // HEAD
if (bodyForbidden) {
  stream.respond(headers, { endStream: true }); // headers only, no fd payload
  return;
}
stream.respondWithFD(fd, headers);

Type guard

function allowsPayload(stream, statusCode) {
  return statusCode !== 204 && statusCode !== 205 && statusCode !== 304 && !stream.headRequest;
}

Try / catch

try {
  stream.respondWithFD(fd, headers);
} catch (err) {
  if (err.code === "ERR_HTTP2_PAYLOAD_FORBIDDEN") {
    stream.respond({ ":status": 304 }, { endStream: true });
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Serving a file with ':status': 304 for cache revalidation; responding with a file on a HEAD request; defaulting headers to 204 in a helper reused by the file-serving path.

Common situations: Static-file servers implementing If-None-Match/If-Modified-Since revalidation by just swapping the status to 304; middleware that normalizes statuses; caching layers forwarding 304 from upstream.

Related errors


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