{"record":{"id":"b2ee97b720b05f73","repo":"denoland/deno","slug":"err-http2-payload-forbidden","errorCode":"ERR_HTTP2_PAYLOAD_FORBIDDEN","errorMessage":"Responses with ${statusCode} status must not have a payload","messagePattern":"Responses with (.+?) status must not have a payload","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/http2.ts","lineNumber":3333,"sourceCode":"    debugStreamObj(this, \"initiating response from fd\");\n    this[kUpdateTimer]();\n    this.ownsFd = false;\n\n    const {\n      headers,\n      statusCode,\n    } = prepareResponseHeadersObject(headersParam, options);\n\n    setOtelServerStatus(this, statusCode);\n\n    // Payload/DATA frames are not permitted in these cases\n    if (\n      statusCode === HTTP_STATUS_NO_CONTENT ||\n      statusCode === HTTP_STATUS_RESET_CONTENT ||\n      statusCode === HTTP_STATUS_NOT_MODIFIED ||\n      this.headRequest\n    ) {\n      throw new ERR_HTTP2_PAYLOAD_FORBIDDEN(statusCode);\n    }\n\n    if (options.statCheck !== undefined) {\n      fs.fstat(\n        fd,\n        FunctionPrototypeBind(\n          doSendFD,\n          this,\n          session,\n          options,\n          fd,\n          headers,\n          streamOptions,\n        ),\n      );\n      return;\n    }\n","sourceCodeStart":3315,"sourceCodeEnd":3351,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/node/polyfills/http2.ts#L3315-L3351","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nif (notModified) {\n  stream.respondWithFD(fd, { \":status\": 304, etag });\n}\n\n// after\nif (notModified) {\n  stream.respond({ \":status\": 304, etag }, { endStream: true });\n  return;\n}","handlingStrategy":"validation","validationCode":"const bodyForbidden =\n  statusCode === 204 || statusCode === 205 || statusCode === 304 ||\n  stream.headRequest; // HEAD\nif (bodyForbidden) {\n  stream.respond(headers, { endStream: true }); // headers only, no fd payload\n  return;\n}\nstream.respondWithFD(fd, headers);","typeGuard":"function allowsPayload(stream, statusCode) {\n  return statusCode !== 204 && statusCode !== 205 && statusCode !== 304 && !stream.headRequest;\n}","tryCatchPattern":"try {\n  stream.respondWithFD(fd, headers);\n} catch (err) {\n  if (err.code === \"ERR_HTTP2_PAYLOAD_FORBIDDEN\") {\n    stream.respond({ \":status\": 304 }, { endStream: true });\n    return;\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["http2","node-compat","http-semantics","file-serving","cache","deno"],"backgroundTag":"http-status-body-forbidden","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}