What is the problem this feature would solve?
HttpClientRequest lets you add/override headers (setHeader, setHeaders) but provides no way to remove a header or transform the header set after a request has been built. The Headers module already has Headers.remove (and set/setAll/merge), but HttpClientRequest doesn't expose an equivalent, so there's no public, prototype-safe way to drop a single header from an existing request.
This isn't hypothetical — it bit us in production. HttpClientRequest.bodyUnsafeJson (→ setBody) writes an explicit content-length header derived from the body's byte length:
// @effect/platform/internal/httpClientRequest.js — setBody
const contentLength = body.contentLength
if (contentLength) {
headers = Headers.set(headers, "content-length", contentLength.toString())
}
When that request is executed through FetchHttpClient, the platform fetch/undici also derives and appends its own content-length from the body. On Node 20, undici then sees a duplicated content-length: <n>, <n> and rejects the request with:
RequestError (reason: Transport)
└─ TypeError: fetch failed
└─ InvalidArgumentError UND_ERR_INVALID_ARG 'invalid content-length header'
(Node 22/24's undici is more lenient, so it only reproduces on Node 20 — which is exactly where many CI runners and Lambda runtimes sit.)
The natural fix is to remove the redundant explicit content-length header before execution and let fetch/undici compute it exactly once. But there's no API to do that. HttpClientRequest.modify only applies set-style operations (it routes headers through setHeaders), and there's no removeHeader/updateHeaders.
What is the feature you are proposing to solve the problem?
Add header-removal / header-transformation combinators to HttpClientRequest, mirroring the existing setHeader/setHeaders and delegating to the Headers module that already has the needed primitives:
// Remove one (or several) headers by key — mirrors Headers.remove
export const removeHeader: {
(key: string): (self: HttpClientRequest) => HttpClientRequest
(self: HttpClientRequest, key: string): HttpClientRequest
}
// General-purpose: transform the whole header set — the most flexible primitive
export const updateHeaders: {
(f: (headers: Headers.Headers) => Headers.Headers): (self: HttpClientRequest) => HttpClientRequest
(self: HttpClientRequest, f: (headers: Headers.Headers) => Headers.Headers): HttpClientRequest
}
updateHeaders is the more fundamental of the two — removeHeader, setHeader, etc. can all be expressed in terms of it — and it composes cleanly with the existing Headers combinators:
import { Headers, HttpClientRequest } from "@effect/platform"
request.pipe(HttpClientRequest.removeHeader("content-length"))
// or, equivalently:
request.pipe(HttpClientRequest.updateHeaders(Headers.remove("content-length")))
This also fixes the motivating case ergonomically, e.g. via HttpClient.mapRequest:
const client = HttpClient.mapRequest(
baseClient,
HttpClientRequest.removeHeader("content-length")
)
(Optionally, HttpClientRequest.modify's options could also gain a removeHeaders?: ReadonlyArray<string> field for symmetry, but removeHeader/updateHeaders are the core asks.)
What alternatives have you considered?
-
Manual clone (current workaround). Because makeInternal builds requests via Object.create(Proto) and there's no public constructor that takes pre-built headers without re-running setBody (which would re-add content-length), we reconstruct the request by hand while preserving the prototype:
import { Headers, HttpClientRequest } from "@effect/platform"
const removeHeader = (
request: HttpClientRequest.HttpClientRequest,
name: string
): HttpClientRequest.HttpClientRequest => {
const headers = Headers.remove(request.headers, name)
// Preserve the HttpClientRequest prototype (TypeId, pipe) while swapping headers.
const next = Object.create(Object.getPrototypeOf(request))
return Object.assign(next, request, { headers })
}
This works but reaches past the public API and is fragile to internal representation changes — exactly the kind of thing a first-class combinator should make unnecessary.
-
Rebuild via HttpClientRequest.make/modify. Not viable: passing body back through modify/make re-runs setBody, which re-adds the content-length header we're trying to remove. There's no public path to set headers after the body without the body re-injecting them.
-
Fix FetchHttpClient to not forward a content-length it already set. This is a reasonable orthogonal improvement (and arguably a separate bug report), but a general removeHeader/updateHeaders is broadly useful well beyond this one case — conditional auth/trace headers, redaction, normalizing proxy headers, stripping hop-by-hop headers, etc. — and brings HttpClientRequest to parity with the Headers module and with server-side response builders.
Environment
@effect/platform 0.96.1, effect 3.21.2
- Node.js 20 (where undici's stricter
content-length validation surfaces the duplicate-header rejection)
What is the problem this feature would solve?
HttpClientRequestlets you add/override headers (setHeader,setHeaders) but provides no way to remove a header or transform the header set after a request has been built. TheHeadersmodule already hasHeaders.remove(andset/setAll/merge), butHttpClientRequestdoesn't expose an equivalent, so there's no public, prototype-safe way to drop a single header from an existing request.This isn't hypothetical — it bit us in production.
HttpClientRequest.bodyUnsafeJson(→setBody) writes an explicitcontent-lengthheader derived from the body's byte length:When that request is executed through
FetchHttpClient, the platformfetch/undici also derives and appends its owncontent-lengthfrom the body. On Node 20, undici then sees a duplicatedcontent-length: <n>, <n>and rejects the request with:(Node 22/24's undici is more lenient, so it only reproduces on Node 20 — which is exactly where many CI runners and Lambda runtimes sit.)
The natural fix is to remove the redundant explicit
content-lengthheader before execution and let fetch/undici compute it exactly once. But there's no API to do that.HttpClientRequest.modifyonly applies set-style operations (it routesheadersthroughsetHeaders), and there's noremoveHeader/updateHeaders.What is the feature you are proposing to solve the problem?
Add header-removal / header-transformation combinators to
HttpClientRequest, mirroring the existingsetHeader/setHeadersand delegating to theHeadersmodule that already has the needed primitives:updateHeadersis the more fundamental of the two —removeHeader,setHeader, etc. can all be expressed in terms of it — and it composes cleanly with the existingHeaderscombinators:This also fixes the motivating case ergonomically, e.g. via
HttpClient.mapRequest:(Optionally,
HttpClientRequest.modify's options could also gain aremoveHeaders?: ReadonlyArray<string>field for symmetry, butremoveHeader/updateHeadersare the core asks.)What alternatives have you considered?
Manual clone (current workaround). Because
makeInternalbuilds requests viaObject.create(Proto)and there's no public constructor that takes pre-built headers without re-runningsetBody(which would re-addcontent-length), we reconstruct the request by hand while preserving the prototype:This works but reaches past the public API and is fragile to internal representation changes — exactly the kind of thing a first-class combinator should make unnecessary.
Rebuild via
HttpClientRequest.make/modify. Not viable: passingbodyback throughmodify/makere-runssetBody, which re-adds thecontent-lengthheader we're trying to remove. There's no public path to set headers after the body without the body re-injecting them.Fix
FetchHttpClientto not forward acontent-lengthit already set. This is a reasonable orthogonal improvement (and arguably a separate bug report), but a generalremoveHeader/updateHeadersis broadly useful well beyond this one case — conditional auth/trace headers, redaction, normalizing proxy headers, stripping hop-by-hop headers, etc. — and bringsHttpClientRequestto parity with theHeadersmodule and with server-side response builders.Environment
@effect/platform0.96.1,effect3.21.2content-lengthvalidation surfaces the duplicate-header rejection)