@effect/platform: HttpClientRequest emits explicit Content-Length, causing wire mismatches under undici 8.2+
メンテナーはふだん 1 日以内に返信
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 72/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 静か
- 技術スタック
- node.js, typescript
調査の方向性
packages/platform/src/internal/httpClientRequest.ts から始め、setBody に注目し、その後 internal/httpBody.ts で作成される body 型を確認してください。提供されている bodyUrlParams の例を使用して、Node 22 と undici 8.2.x で問題を再現してください。完了の条件は、送信 body ヘルパーが明示的な Content-Length を提供せず、transport が一致する wire 値を計算することです。
索引モデルが issue の本文から書いたものです。
説明
Summary
@effect/platform's HttpClientRequest body helpers (bodyUrlParams, bodyUnsafeJson, bodyText, bodyUint8Array) attach an explicit content-length header to outbound requests, computed at request-build time from the encoded Uint8Array.length. The transport (fetch / undici) is supposed to compute that header itself. With undici 8.2.0+ this preempting causes real-world breakage because the value Effect snapshots no longer matches the bytes actually written to the wire.
Where it happens
In packages/platform/src/internal/httpClientRequest.ts, in setBody:
const contentLength = body.contentLength;
if (contentLength) {
headers = Headers.set(headers, "content-length", contentLength.toString());
}
The Uint8ArrayImpl body type (created by text, uint8Array, unsafeJson, urlParams in internal/httpBody.ts) exposes contentLength from Uint8Array.length. So every request built with the form/json/text helpers carries a caller-supplied content-length header.
Why this matters
The header value Effect emits isn't malformed — it's a correct digit string at the moment Effect builds the request. The problem is that Effect is preempting work the transport owns:
- The WHATWG Fetch spec puts
Content-Lengthon the forbidden request-header list precisely so the implementation can compute the canonical value from what it's actually going to send (after body extraction, interceptor cloning, redirect handling, stream wrapping, OTel instrumentation, etc.). - Before undici 8.2.0, undici would always recompute its own
content-lengthand overwrite the caller-supplied one, so any drift between Effect's snapshot and the transport's actual byte count was silently corrected. - undici 8.2.0 (PR nodejs/undici#5060) stopped auto-overriding when the caller already set the header. Now Effect's value goes to the wire unchanged. If anything between the
body.lengthsnapshot and the bytes leaving the socket differs by even one byte, the upstream server gets a mismatchedContent-Lengthand rejects the request.
We observed this in production against multiple unrelated third-party APIs that share only the bodyUrlParams code path. Pinning undici to 8.1.0 restored the prior lenient behavior. Note this reproduces on Node 22 even though Node ships its own bundled undici: the moment any dependency does import "undici" (e.g. @vercel/blob, @workflow/world-vercel), userland undici installs itself as the global dispatcher and the new behavior applies process-wide.
Workaround
Replace bodyUrlParams({...}) with HttpBody.raw(new URLSearchParams(...), { contentType: "application/x-www-form-urlencoded" }). HttpBody.raw doesn't expose a contentLength unless you pass it explicitly, so setBody skips emitting the header, and the transport computes it correctly.
Suggested fix
Stop emitting content-length from body.contentLength in setBody. The contentLength field is still useful internally (stream framing, server responses), but it shouldn't be reflected into outbound request headers — the transport is the only layer that can compute a value that matches what's actually sent.
export const setBody = dual(2, (self, body) => {
let headers = self.headers;
if (body._tag === "Empty" || body._tag === "FormData") {
headers = Headers.remove(headers, ["Content-type", "Content-length"]);
} else {
const contentType = body.contentType;
if (contentType) {
headers = Headers.set(headers, "content-type", contentType);
}
// removed: explicit content-length header — let the transport compute it
}
return makeInternal(self.method, self.url, self.urlParams, self.hash, headers, body);
});
Reproduction
Against Node 22 + undici 8.2.x:
import { FetchHttpClient, HttpClient, HttpClientRequest } from "@effect/platform"
import { Effect } from "effect"
// Any dep that imports "undici" installs userland 8.x as global dispatcher
await import("undici")
const program = Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
return yield* http.execute(
HttpClientRequest.post("https://api.example.com/oauth/token").pipe(
HttpClientRequest.bodyUrlParams({ grant_type: "refresh_token", refresh_token: "..." })
)
)
})
await Effect.runPromise(program.pipe(Effect.provide(FetchHttpClient.layer)))
// Upstream rejects with a Content-Length mismatch
Pinning userland undici to 8.1.0 resolves it without any Effect change.
Environment
@effect/platform: 0.96.xeffect: latestnode: 22.xundici(userland, transitive): 8.2.0+
- 主要言語
- TypeScript
- スター
- 16.7k
- フォーク
- 808
- 平均マージ
- 11時間 3分
- マージ済み PR(30日)
- 839
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
Effect-TS/effect のほかの issue
-
Support OpenTelemetry JS SDK v3 and replace deprecated tracing peers in Effect v4対応中かも @effect-bot が今日担当しました。 オープン
Effect-TS/effect#10328 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
PersistedQueue: offer option that de-duplicates an id only while it is pending or processing対応中かも @effect-bot が 1 日前に担当しました。 オープン
Effect-TS/effect#10313 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
shorthand for mapping over tagged union schema対応中かも @effect-bot が 2 日前に担当しました。 オープンenhancement
Effect-TS/effect#10286 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Cluster: A way to remove processed requests from storage対応中かも @effect-bot が 3 日前に担当しました。 オープンenhancement
Effect-TS/effect#10239 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
Effect-TS/effect の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
caprover/caprover-frontend#273 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
ai-driven-dev/framework#1147 ·
メンテナーはふだん 1 日以内に返信
-
agent-ui bug p2
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
cybersemics/em#6232 ·
メンテナーはふだん 1 日以内に返信