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

  1. Guard with if (stream.headersSent) return; before additionalHeaders()
  2. Emit hints at the very start of the request lifecycle, before any heavy work
  3. For post-body headers use trailers: respond(headers, { waitForTrailers: true }) and stream.on('wantTrailers', ...)
  4. 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

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


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