denoland/deno · error · Error

ERR_HTTP2_HEADERS_SENT

ERR_HTTP2_HEADERS_SENT

Error message

Response has already been initiated.

What it means

Thrown by Http2Stream.respond() when response headers have already been sent on that stream. A stream may be responded to exactly once; the headersSent guard makes any second respond() (or respondWithFile/respondWithFD after respond) illegal.

Source

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

      callback(null, stream, headers, 0);
    });

    if (onServerStreamCreatedChannel.hasSubscribers) {
      onServerStreamCreatedChannel.publish({
        stream,
        headers,
      });
    }
  }

  // Initiate a response on this Http2Stream
  respond(headersParam, options) {
    if (this.destroyed || this.closed) {
      throw new ERR_HTTP2_INVALID_STREAM();
    }
    if (this.headersSent) {
      throw new ERR_HTTP2_HEADERS_SENT();
    }

    const state = this[kState];

    assertIsObject(options, "options");
    options = { ...options };

    debugStreamObj(this, "initiating response");
    this[kUpdateTimer]();

    options.endStream = !!options.endStream;

    let streamOptions = 0;
    if (options.endStream) {
      streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
      state.endStream = true;
    }

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Check stream.headersSent before any respond/respondWithFile/respondWithFD call
  2. Audit middleware and error handlers: they must skip writing if headersSent is true
  3. Centralize responding in one function per stream so only one code path can respond
  4. For premature-end cases use stream.end() instead of a second respond()

Example fix

// before
if (cached) stream.respond({ ":status": 200, "content-type": "text/plain" });
stream.respond({ ":status": 200, "content-type": "application/json" });

// after
if (!stream.headersSent) {
  stream.respond({ ":status": 200, "content-type": "application/json" });
}
stream.end(body);
Defensive patterns

Strategy: validation

Validate before calling

if (stream.headersSent) {
  // headers already went out; only more body data is allowed now
  return;
}
stream.respond(headers);

Type guard

function canRespond(stream) {
  return !stream.headersSent && !stream.destroyed && !stream.closed;
}

Try / catch

try {
  stream.respond(headers);
} catch (err) {
  if (err.code === "ERR_HTTP2_HEADERS_SENT") {
    // already responded earlier in the pipeline; just end the stream
    stream.end();
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling stream.respond() twice in the same handler; middleware that responds and then lets the main handler respond again; an error handler firing after the success path already called respond(); mixing respond() and respondWithFile() on one stream.

Common situations: Express-style middleware ported to http2 where both the auth layer and route write a response; catch blocks that unconditionally send a 500 even when the response already started; copy-pasted respond calls in branching code paths that both execute.

Related errors


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