denoland/deno · error · NodeError
ERR_HTTP_HEADERS_SENT
ERR_HTTP_HEADERS_SENT
Error message
Cannot render headers after they are sent to the client
What it means
Inside ClientRequest's header-rendering path for requests carrying an 'expect' header (Expect: 100-continue), the code refuses to render headers twice: if this._header is already set when it needs to call _storeHeader again, it throws ERR_HTTP_HEADERS_SENT with the 'render' context. _header is assigned once headers have been serialized toward the socket, so this indicates a second rendering attempt after the request head was already produced.
Source
Thrown at ext/node/polyfills/_http_client.js:767
if (this[kProxy] && protocol === "http:") {
// Mirror what _storeHeader will pick for Connection: when shouldKeepAlive
// is true, both Connection and Proxy-Connection are "keep-alive"; when
// false, both are "close". Matches Node's wire format on the proxy hop.
if (!this.getHeader("proxy-connection")) {
this.setHeader(
"Proxy-Connection",
this.shouldKeepAlive ? "keep-alive" : "close",
);
}
if (this[kProxy].auth && !this.getHeader("proxy-authorization")) {
this.setHeader("Proxy-Authorization", this[kProxy].auth);
}
}
if (this.getHeader("expect")) {
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT("render");
}
this._storeHeader(
this.method + " " + this.path + " HTTP/1.1\r\n",
this[kOutHeaders],
);
}
} else {
this._storeHeader(
this.method + " " + this.path + " HTTP/1.1\r\n",
options.headers,
);
}
this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
// Save options for potential stale keepalive retry
this[kRetryOptions] = optsWithoutSignal;View on GitHub (pinned to 89f33cbef2)
Solutions
- Guard follow-up writes with if (!req.headersSent) before writing or re-rendering
- Do not reuse a ClientRequest object for retries — create a new request instead
- Drop the expect header unless you specifically implement the 100-continue handshake
Example fix
// before
req.on('error', () => { req.setHeader('retry', '1'); req.end(body); }); // after headers sent
// after
req.on('error', () => {
if (!req.headersSent) { req.setHeader('retry', '1'); req.end(body); }
else { const retry = http.request(req.getHeaders()); retry.end(body); }
}); Defensive patterns
Strategy: validation
Validate before calling
if (req.getHeader('expect') && req.headersSent) {
throw new Error('request head already rendered; create a new request');
}
// only write/end while headersSent is false Try / catch
req.on('error', (e) => { if (e.code === 'ERR_HTTP_HEADERS_SENT') { /* abort this request, reissue a fresh ClientRequest */ } else throw e; }); Prevention
- Check headersSent before any late write with expect: 100-continue
- One request object per attempt; never re-render after flush
- Implement 100-continue with the 'checkContinue' event instead of manual re-entry
When it happens
Trigger: Using expect: '100-continue' and then triggering another implicit/explicit header generation after the head was sent — e.g. calling write()/end() again after headers flushed, or invoking internal header rendering twice via flows that re-enter _implicitHeader.
Common situations: Retry logic around 100-continue that re-invokes the request lifecycle on the same object; middleware that writes after the request was already flushed; double-processing of a paused/resumed request with expect set.
Related errors
- ERR_HTTP_HEADERS_SENT
- ERR_INVALID_ARG_TYPE
- ERR_INVALID_URL
- ERR_UNESCAPED_CHARACTERS
- ERR_INVALID_ARG_TYPE
AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16).
Data as JSON: /api/errors/08ca817326bb5da1.
Report an issue: GitHub.