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
- For 204/205/304 call stream.respond(headers, { endStream: true }) instead — no payload API at all
- Skip the body entirely for HEAD requests: respond with endStream and never call respondWithFD
- Branch on req.method and status before choosing respondWithFD vs respond
- 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
- Branch on revalidation result before choosing the serving API: 304 path never serves a file
- Answer HEAD requests with respond() + endStream, never a payload API
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
- ERR_INVALID_ARG_TYPE
- ERR_HTTP2_HEADERS_SENT
- ERR_HTTP2_HEADERS_AFTER_RESPOND
- ERR_HTTP2_STATUS_101
- ERR_HTTP2_INVALID_INFO_STATUS
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/b2ee97b720b05f73.
Report an issue: GitHub.