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

  1. Guard follow-up writes with if (!req.headersSent) before writing or re-rendering
  2. Do not reuse a ClientRequest object for retries — create a new request instead
  3. 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

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


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/08ca817326bb5da1. Report an issue: GitHub.