denoland/deno · error · Error
ERR_HTTP2_HEADERS_AFTER_RESPOND
ERR_HTTP2_HEADERS_AFTER_RESPOND
Error message
Cannot specify additional headers after response initiated
What it means
additionalHeaders() may only send informational headers *before* the final response headers. Once headersSent is true (respond/respondWithFile/respondWithFD already ran), calling it throws ERR_HTTP2_HEADERS_AFTER_RESPOND — HEADERS after DATA violate request/response semantics in HTTP/2.
Source
Thrown at ext/node/polyfills/http2.ts:3449
headers,
streamOptions,
),
);
}
// Sends a block of informational headers. In theory, the HTTP/2 spec
// allows sending a HEADER block at any time during a streams lifecycle,
// but the HTTP request/response semantics defined in HTTP/2 places limits
// such that HEADERS may only be sent *before* or *after* DATA frames.
// If the block of headers being sent includes a status code, it MUST be
// a 1xx informational code and it MUST be sent before the request/response
// headers are sent, or an error will be thrown.
additionalHeaders(headers) {
if (this.destroyed || this.closed) {
throw new ERR_HTTP2_INVALID_STREAM();
}
if (this.headersSent) {
throw new ERR_HTTP2_HEADERS_AFTER_RESPOND();
}
assertIsObject(headers, "headers");
headers = ObjectAssign({ __proto__: null }, headers);
debugStreamObj(this, "sending additional headers");
if (headers[HTTP2_HEADER_STATUS] != null) {
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
if (statusCode === HTTP_STATUS_SWITCHING_PROTOCOLS) {
throw new ERR_HTTP2_STATUS_101();
}
if (statusCode < 100 || statusCode >= 200) {
throw new ERR_HTTP2_INVALID_INFO_STATUS(headers[HTTP2_HEADER_STATUS]);
}
}
this[kUpdateTimer]();View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Guard with if (stream.headersSent) return; before additionalHeaders()
- Emit hints at the very start of the request lifecycle, before any heavy work
- For post-body headers use trailers: respond(headers, { waitForTrailers: true }) and stream.on('wantTrailers', ...)
- Reorder middleware so hints precede the responder
Example fix
// before
stream.respond({ ":status": 200 });
stream.end(body);
stream.additionalHeaders({ ":status": 103, link: "</a.js>; rel=preload" });
// after
if (!stream.headersSent) {
stream.additionalHeaders({ ":status": 103, link: "</a.js>; rel=preload" });
}
stream.respond({ ":status": 200 });
stream.end(body); Defensive patterns
Strategy: validation
Validate before calling
if (stream.headersSent) {
return; // final response already started; hints no longer allowed
}
stream.additionalHeaders(hints); Type guard
function canSendInfo(stream) {
return !stream.headersSent && !stream.destroyed && !stream.closed;
} Try / catch
try {
stream.additionalHeaders(hints);
} catch (err) {
if (err.code === "ERR_HTTP2_HEADERS_AFTER_RESPOND") return; // too late for hints
throw err;
} Prevention
- Order hint middleware before anything that can respond
- For after-body metadata use trailers (waitForTrailers + 'wantTrailers'), not additionalHeaders
When it happens
Trigger: Sending 103 Early Hints after already calling respond(); a hint middleware ordered after the responding handler; sending trailer-style headers via additionalHeaders instead of the waitForTrailers mechanism.
Common situations: Middleware pipelines where hint emission happens too late; confusion between informational headers (before response) and trailers (after body, which need options.waitForTrailers on respond).
Related errors
- ERR_HTTP2_HEADERS_SENT
- ERR_HTTP2_INVALID_INFO_STATUS
- ERR_INVALID_ARG_TYPE
- ERR_HTTP2_PAYLOAD_FORBIDDEN
- ERR_HTTP2_STATUS_101
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/d392bc718fcf9b59.
Report an issue: GitHub.