denoland/deno · error · Error
ERR_HTTP2_INVALID_STREAM
ERR_HTTP2_INVALID_STREAM
Error message
The stream has been destroyed
What it means
http2stream.sendTrailers(headers) throws ERR_HTTP2_INVALID_STREAM ('The stream has been destroyed') as its first guard when stream.destroyed or stream.closed is true. Trailers are trailing HEADERS frames and can only be sent on a live (half-closed) stream; once the peer sent RST_STREAM or the stream closed, there is nothing to attach them to.
Source
Thrown at ext/node/polyfills/http2.ts:2466
if (this.destroyed) {
// deno-lint-ignore deno-internal/prefer-primordials
this.push(null);
return;
}
if (!this[kState].didRead) {
this._readableState.readingMore = false;
this[kState].didRead = true;
}
if (!this.pending) {
FunctionPrototypeCall(streamOnResume, this);
} else {
this.once("ready", streamOnResume);
}
}
sendTrailers(headers) {
if (this.destroyed || this.closed) {
throw new ERR_HTTP2_INVALID_STREAM();
}
if (this[kSentTrailers]) {
throw new ERR_HTTP2_TRAILERS_ALREADY_SENT();
}
if (!this[kState].trailersReady) {
throw new ERR_HTTP2_TRAILERS_NOT_READY();
}
assertIsObject(headers, "headers");
headers = ObjectAssign({ __proto__: null }, headers);
debugStreamObj(this, "sending trailers");
this[kUpdateTimer]();
const headersList = buildNgHeaderString(
headers,
assertValidPseudoHeaderTrailer,View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Check liveness before sending: if (!stream.destroyed && !stream.closed) stream.sendTrailers(headers)
- Send trailers only from inside the 'wantTrailers' listener, which only fires while the stream is alive
- Wrap the send in try/catch and treat ERR_HTTP2_INVALID_STREAM as a benign lost-stream race
Example fix
// before
stream.on('wantTrailers', async () => {
const checksum = await hashBody(); // client may abort meanwhile
stream.sendTrailers({ 'x-checksum': checksum }); // throws if closed
});
// after
stream.on('wantTrailers', async () => {
const checksum = await hashBody();
if (!stream.destroyed && !stream.closed) {
stream.sendTrailers({ 'x-checksum': checksum });
}
}); Defensive patterns
Strategy: validation
Validate before calling
const sendTrailersIfLive = (stream: Http2Stream, headers: Record<string, string>) => {
if (!stream.destroyed && !stream.closed) {
stream.sendTrailers(headers);
return true;
}
return false;
}; Type guard
const isLiveHttp2Stream = (s: Http2Stream): boolean => !s.destroyed && !s.closed;
Try / catch
try {
stream.sendTrailers(headers);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ERR_HTTP2_INVALID_STREAM') {
// client reset the stream; the trailers are simply lost — safe to ignore
} else throw err;
} Prevention
- Send trailers only from the 'wantTrailers' listener
- Treat client aborts as expected: check stream liveness after any await
- Drop references to streams on their 'close' event
When it happens
Trigger: Calling sendTrailers after the client aborted (RST_STREAM) mid-request; awaiting an async operation (db commit, hash) inside the 'wantTrailers' handler that resolves after the stream closed; sending trailers from a 'close' handler.
Common situations: Servers computing checksum/metadata trailers from a slow source while an impatient client times out and resets the stream; shared trailer-sending helpers invoked in finally blocks; retries that race stream teardown.
Related errors
- ERR_HTTP2_TRAILERS_ALREADY_SENT
- ERR_HTTP2_TRAILERS_NOT_READY
- ERR_HTTP2_HEADERS_SENT
- ERR_INVALID_HTTP_TOKEN
- ERR_INVALID_CHAR
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/4786850b1a74ad2d.
Report an issue: GitHub.