{"record":{"id":"c0fa1264da15797d","repo":"denoland/deno","slug":"err-http2-trailers-not-ready","errorCode":"ERR_HTTP2_TRAILERS_NOT_READY","errorMessage":"Trailing headers cannot be sent until after the wantTrailers event is emitted","messagePattern":"Trailing headers cannot be sent until after the wantTrailers event is emitted","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/http2.ts","lineNumber":2472,"sourceCode":"      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;\n\n    // Send the trailers in setImmediate so we don't do it on nghttp2 stack.\n    setImmediate(finishSendTrailers, this, headersList);","sourceCodeStart":2454,"sourceCodeEnd":2490,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/node/polyfills/http2.ts#L2454-L2490","documentation":"sendTrailers throws ERR_HTTP2_TRAILERS_NOT_READY until the stream signals readiness. The trailersReady flag is only set in onStreamTrailers, which runs when the stream finishes sending data AND the stream was created with { waitForTrailers: true } in respond()/respondWithFD()/respondWithFile() options — that is also what emits the 'wantTrailers' event. Calling sendTrailers before that moment (typically right after respond) is premature.","triggerScenarios":"stream.respond(headers) without waitForTrailers, then stream.sendTrailers(...) after end() — readiness never becomes true; calling sendTrailers immediately after respond() before the body finished; using respondWithFD and sending trailers before the file fully streamed.","commonSituations":"Code ported from HTTP/1 chunked trailers where you append trailers after the last chunk; developers assuming trailers can be queued any time after headers; forgetting the waitForTrailers opt-in entirely.","solutions":["Pass { waitForTrailers: true } to respond(): stream.respond(headers, { waitForTrailers: true })","Send trailers exclusively inside the 'wantTrailers' event listener, which fires exactly when the stream is ready","Register the listener before respond/end so the auto empty-trailers path is not taken"],"exampleFix":"// before\nstream.respond({ ':status': 200 });\nstream.end(body);\nstream.sendTrailers({ 'x-checksum': sum }); // throws TRAILERS_NOT_READY\n\n// after\nstream.on('wantTrailers', () => stream.sendTrailers({ 'x-checksum': sum }));\nstream.respond({ ':status': 200 }, { waitForTrailers: true });\nstream.end(body);","handlingStrategy":"validation","validationCode":"if (!respondedWithWaitForTrailers) {\n  throw new Error('pass { waitForTrailers: true } to respond() before sending trailers');\n}\n// readiness = the 'wantTrailers' event fired\nstream.on('wantTrailers', () => stream.sendTrailers(trailerHeaders));\nstream.respond(headers, { waitForTrailers: true });\nstream.end(body);","typeGuard":null,"tryCatchPattern":"try {\n  stream.sendTrailers(headers);\n} catch (err) {\n  const code = (err as NodeJS.ErrnoException).code;\n  if (code === 'ERR_HTTP2_TRAILERS_NOT_READY') {\n    // defer: retry from the 'wantTrailers' listener instead of now\n  } else throw err;\n}","preventionTips":["Always pair sendTrailers with respond(..., { waitForTrailers: true }) and a 'wantTrailers' listener","Never send trailers ad hoc after end(); use the event contract","Encapsulate the pattern in one helper so call sites cannot get it wrong"],"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"}