{"record":{"id":"8a42358aa7332426","repo":"denoland/deno","slug":"err-http2-headers-sent","errorCode":"ERR_HTTP2_HEADERS_SENT","errorMessage":"Response has already been initiated.","messagePattern":"Response has already been initiated\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/http2.ts","lineNumber":3201,"sourceCode":"\n      callback(null, stream, headers, 0);\n    });\n\n    if (onServerStreamCreatedChannel.hasSubscribers) {\n      onServerStreamCreatedChannel.publish({\n        stream,\n        headers,\n      });\n    }\n  }\n\n  // Initiate a response on this Http2Stream\n  respond(headersParam, options) {\n    if (this.destroyed || this.closed) {\n      throw new ERR_HTTP2_INVALID_STREAM();\n    }\n    if (this.headersSent) {\n      throw new ERR_HTTP2_HEADERS_SENT();\n    }\n\n    const state = this[kState];\n\n    assertIsObject(options, \"options\");\n    options = { ...options };\n\n    debugStreamObj(this, \"initiating response\");\n    this[kUpdateTimer]();\n\n    options.endStream = !!options.endStream;\n\n    let streamOptions = 0;\n    if (options.endStream) {\n      streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;\n      state.endStream = true;\n    }\n","sourceCodeStart":3183,"sourceCodeEnd":3219,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/node/polyfills/http2.ts#L3183-L3219","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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()"],"exampleFix":"// before\nif (cached) stream.respond({ \":status\": 200, \"content-type\": \"text/plain\" });\nstream.respond({ \":status\": 200, \"content-type\": \"application/json\" });\n\n// after\nif (!stream.headersSent) {\n  stream.respond({ \":status\": 200, \"content-type\": \"application/json\" });\n}\nstream.end(body);","handlingStrategy":"validation","validationCode":"if (stream.headersSent) {\n  // headers already went out; only more body data is allowed now\n  return;\n}\nstream.respond(headers);","typeGuard":"function canRespond(stream) {\n  return !stream.headersSent && !stream.destroyed && !stream.closed;\n}","tryCatchPattern":"try {\n  stream.respond(headers);\n} catch (err) {\n  if (err.code === \"ERR_HTTP2_HEADERS_SENT\") {\n    // already responded earlier in the pipeline; just end the stream\n    stream.end();\n    return;\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["http2","node-compat","headers-sent","stream-lifecycle","deno"],"backgroundTag":"response-already-sent","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}