{"record":{"id":"516197e49fb620cb","repo":"denoland/deno","slug":"err-http2-trailers-already-sent","errorCode":"ERR_HTTP2_TRAILERS_ALREADY_SENT","errorMessage":"Trailing headers have already been sent","messagePattern":"Trailing headers have already been sent","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/http2.ts","lineNumber":2469,"sourceCode":"      return;\n    }\n    if (!this[kState].didRead) {\n      this._readableState.readingMore = false;\n      this[kState].didRead = true;\n    }\n    if (!this.pending) {\n      FunctionPrototypeCall(streamOnResume, this);\n    } else {\n      this.once(\"ready\", streamOnResume);\n    }\n  }\n\n  sendTrailers(headers) {\n    if (this.destroyed || this.closed) {\n      throw new ERR_HTTP2_INVALID_STREAM();\n    }\n    if (this[kSentTrailers]) {\n      throw new ERR_HTTP2_TRAILERS_ALREADY_SENT();\n    }\n    if (!this[kState].trailersReady) {\n      throw new ERR_HTTP2_TRAILERS_NOT_READY();\n    }\n\n    assertIsObject(headers, \"headers\");\n    headers = ObjectAssign({ __proto__: null }, headers);\n\n    debugStreamObj(this, \"sending trailers\");\n\n    this[kUpdateTimer]();\n\n    const headersList = buildNgHeaderString(\n      headers,\n      assertValidPseudoHeaderTrailer,\n      this.session[kStrictSingleValueFields],\n    );\n    this[kSentTrailers] = headers;","sourceCodeStart":2451,"sourceCodeEnd":2487,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/node/polyfills/http2.ts#L2451-L2487","documentation":"sendTrailers can be invoked exactly once per stream: after a successful call, kSentTrailers is set and a second call throws ERR_HTTP2_TRAILERS_ALREADY_SENT. Importantly, onStreamTrailers automatically calls sendTrailers({}) when the 'wantTrailers' event has no listeners, so a late manual send after that automatic empty-trailers frame also counts as a duplicate.","triggerScenarios":"Calling stream.sendTrailers(...) twice (e.g. in both the data-completion path and an error path); registering waitForTrailers: true but attaching the 'wantTrailers' listener after stream.end() already fired, so the polyfill auto-sent {} and the later manual send throws.","commonSituations":"Error-handling paths that send error trailers after normal trailers were already sent; refactoring that moved sendTrailers into two places; listeners attached too late because they were registered after an awaited operation.","solutions":["Register the 'wantTrailers' listener synchronously, before ending the stream, whenever you respond with waitForTrailers: true","Send trailers from exactly one place — inside the 'wantTrailers' listener","Track sent state: if (stream.sentTrailers) return; (the polyfill exposes the flag) before calling sendTrailers"],"exampleFix":"// before\nstream.respond(headers, { waitForTrailers: true });\nstream.end(data);\nstream.sendTrailers({ 'x-total': '42' }); // may race the auto empty-trailers send\n\n// after\nstream.on('wantTrailers', () => {\n  stream.sendTrailers({ 'x-total': '42' }); // single send site\n});\nstream.respond(headers, { waitForTrailers: true });\nstream.end(data);","handlingStrategy":"try-catch","validationCode":"if (!stream.sentTrailers) {\n  stream.sendTrailers(headers);\n}","typeGuard":"const canSendTrailers = (s: Http2Stream): boolean =>\n  !s.destroyed && !s.closed && !s.sentTrailers;","tryCatchPattern":"try {\n  stream.sendTrailers(headers);\n} catch (err) {\n  const code = (err as NodeJS.ErrnoException).code;\n  if (code === 'ERR_HTTP2_TRAILERS_ALREADY_SENT') {\n    // duplicate send; make this code path unreachable by consolidating call sites\n  } else throw err;\n}","preventionTips":["Keep exactly one sendTrailers call site per stream","Register 'wantTrailers' synchronously so the auto empty-trailers send never happens","Check stream.sentTrailers before sending defensively"],"tags":["http2","trailers","api-misuse","node-compat"],"backgroundTag":"trailers-already-sent","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}