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
- Check stream.headersSent before any respond/respondWithFile/respondWithFD call
- Audit middleware and error handlers: they must skip writing if headersSent is true
- Centralize responding in one function per stream so only one code path can respond
- 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
- Make exactly one module own the respond decision per stream
- Every error handler must start with if (stream.headersSent) { stream.end(); return; }
- Return immediately after every respond* call so later branches cannot re-respond
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
- ERR_HTTP2_HEADERS_AFTER_RESPOND
- ERR_HTTP2_INVALID_STREAM
- 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/8a42358aa7332426.
Report an issue: GitHub.