From accace0dd21bf7b0ed614b7af84bfbab9c33e394 Mon Sep 17 00:00:00 2001 From: Xia Chao <236466140+bun-unsafe@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:47:09 +0800 Subject: [PATCH 1/4] fix(http): keep raw Web Response bodies readable in toClientResponse --- .changeset/toclient-raw-response-readable.md | 5 +++++ .../effect/src/unstable/http/HttpServerResponse.ts | 2 +- .../test/unstable/http/HttpServerResponse.test.ts | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/toclient-raw-response-readable.md diff --git a/.changeset/toclient-raw-response-readable.md b/.changeset/toclient-raw-response-readable.md new file mode 100644 index 00000000000..5d9fd4434a8 --- /dev/null +++ b/.changeset/toclient-raw-response-readable.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpServerResponse.toClientResponse` consuming a `raw(Response)` body on the first read. Reads now go through a clone of the wrapped `Response`, so `text`, `json`, `arrayBuffer` and `urlParamsBody` can be read more than once, matching `HttpClientResponse.fromWeb`. diff --git a/packages/effect/src/unstable/http/HttpServerResponse.ts b/packages/effect/src/unstable/http/HttpServerResponse.ts index 948eda7df80..b98dd8d6075 100644 --- a/packages/effect/src/unstable/http/HttpServerResponse.ts +++ b/packages/effect/src/unstable/http/HttpServerResponse.ts @@ -1229,7 +1229,7 @@ class ServerHttpClientResponse extends Inspectable.Class implements HttpClientRe const rawBody = body.body if (rawBody instanceof Response) { return Effect.tryPromise({ - try: () => rawBody.arrayBuffer().then((buffer) => new Uint8Array(buffer)), + try: () => rawBody.clone().arrayBuffer().then((buffer) => new Uint8Array(buffer)), catch: (cause) => this.decodeError(cause) }) } diff --git a/packages/effect/test/unstable/http/HttpServerResponse.test.ts b/packages/effect/test/unstable/http/HttpServerResponse.test.ts index b44082cf5a4..d58c03e2d06 100644 --- a/packages/effect/test/unstable/http/HttpServerResponse.test.ts +++ b/packages/effect/test/unstable/http/HttpServerResponse.test.ts @@ -158,6 +158,20 @@ describe("HttpServerResponse", () => { assert.strictEqual(yield* roundTrip.text, "") })) + it.effect("toClientResponse keeps raw Web Response bodies readable more than once", () => + Effect.gen(function*() { + const clientResponse = HttpServerResponse.toClientResponse( + HttpServerResponse.raw( + new Response("hello", { headers: { "content-type": "text/plain" } }), + { contentType: "text/plain" } + ) + ) + + assert.strictEqual(yield* clientResponse.text, "hello") + assert.strictEqual(yield* clientResponse.text, "hello") + assert.strictEqual((yield* clientResponse.arrayBuffer).byteLength, 5) + })) + it("fromClientResponse ignores malformed or unsafe content lengths", () => { const request = HttpClientRequest.get("http://localhost:3000") for (const contentLength of ["2junk", "1.5", "1e3", "9007199254740992"]) { From ea801de61ecc6c43e26621f21a9f93a0a51cfde3 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Wed, 16 Sep 2026 21:14:43 +0000 Subject: [PATCH 2/4] test(http): focus raw response repeated-read regression --- .../effect/test/unstable/http/HttpServerResponse.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/effect/test/unstable/http/HttpServerResponse.test.ts b/packages/effect/test/unstable/http/HttpServerResponse.test.ts index d58c03e2d06..c30f6b3b6f7 100644 --- a/packages/effect/test/unstable/http/HttpServerResponse.test.ts +++ b/packages/effect/test/unstable/http/HttpServerResponse.test.ts @@ -158,18 +158,15 @@ describe("HttpServerResponse", () => { assert.strictEqual(yield* roundTrip.text, "") })) - it.effect("toClientResponse keeps raw Web Response bodies readable more than once", () => + it.effect("toClientResponse supports repeated reads of raw Web Response bodies", () => Effect.gen(function*() { const clientResponse = HttpServerResponse.toClientResponse( - HttpServerResponse.raw( - new Response("hello", { headers: { "content-type": "text/plain" } }), - { contentType: "text/plain" } - ) + HttpServerResponse.raw(new Response("hello")) ) assert.strictEqual(yield* clientResponse.text, "hello") assert.strictEqual(yield* clientResponse.text, "hello") - assert.strictEqual((yield* clientResponse.arrayBuffer).byteLength, 5) + assert.deepStrictEqual(new Uint8Array(yield* clientResponse.arrayBuffer), new TextEncoder().encode("hello")) })) it("fromClientResponse ignores malformed or unsafe content lengths", () => { From 35e99998f5a62d35ffd402a5f1b9ee70e48b1544 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Wed, 16 Sep 2026 21:24:18 +0000 Subject: [PATCH 3/4] test(http): preserve raw response after client body reads --- .../unstable/http/HttpServerResponse.test.ts | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/effect/test/unstable/http/HttpServerResponse.test.ts b/packages/effect/test/unstable/http/HttpServerResponse.test.ts index c30f6b3b6f7..4dc1c122799 100644 --- a/packages/effect/test/unstable/http/HttpServerResponse.test.ts +++ b/packages/effect/test/unstable/http/HttpServerResponse.test.ts @@ -81,6 +81,27 @@ describe("HttpServerResponse", () => { assert.strictEqual(roundTrip.headers.get("content-length"), "5") })) + describe("toClientResponse", () => { + it.effect("supports repeated reads of raw Web Response bodies", () => + Effect.gen(function*() { + const clientResponse = HttpServerResponse.toClientResponse( + HttpServerResponse.raw(new Response("hello")) + ) + + assert.strictEqual(yield* clientResponse.text, "hello") + assert.strictEqual(yield* clientResponse.text, "hello") + assert.deepStrictEqual(new Uint8Array(yield* clientResponse.arrayBuffer), new TextEncoder().encode("hello")) + })) + + it.effect("leaves the raw Web Response servable after a client-body read", () => + Effect.gen(function*() { + const response = HttpServerResponse.raw(new Response("hello")) + + assert.strictEqual(yield* HttpServerResponse.toClientResponse(response).text, "hello") + assert.strictEqual(yield* Effect.promise(() => HttpServerResponse.toWeb(response).text()), "hello") + })) + }) + it.effect("fromClientResponse preserves status, headers, cookies, and json", () => Effect.gen(function*() { const request = HttpClientRequest.get("http://localhost:3000/todos/1?existing=1", { @@ -158,17 +179,6 @@ describe("HttpServerResponse", () => { assert.strictEqual(yield* roundTrip.text, "") })) - it.effect("toClientResponse supports repeated reads of raw Web Response bodies", () => - Effect.gen(function*() { - const clientResponse = HttpServerResponse.toClientResponse( - HttpServerResponse.raw(new Response("hello")) - ) - - assert.strictEqual(yield* clientResponse.text, "hello") - assert.strictEqual(yield* clientResponse.text, "hello") - assert.deepStrictEqual(new Uint8Array(yield* clientResponse.arrayBuffer), new TextEncoder().encode("hello")) - })) - it("fromClientResponse ignores malformed or unsafe content lengths", () => { const request = HttpClientRequest.get("http://localhost:3000") for (const contentLength of ["2junk", "1.5", "1e3", "9007199254740992"]) { From ba0b802d7b008c8c6f0bd1153b277b59a5941b88 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Wed, 16 Sep 2026 21:28:29 +0000 Subject: [PATCH 4/4] docs(http): clarify raw response release note --- .changeset/toclient-raw-response-readable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/toclient-raw-response-readable.md b/.changeset/toclient-raw-response-readable.md index 5d9fd4434a8..dd8307783ab 100644 --- a/.changeset/toclient-raw-response-readable.md +++ b/.changeset/toclient-raw-response-readable.md @@ -2,4 +2,4 @@ "effect": patch --- -Fix `HttpServerResponse.toClientResponse` consuming a `raw(Response)` body on the first read. Reads now go through a clone of the wrapped `Response`, so `text`, `json`, `arrayBuffer` and `urlParamsBody` can be read more than once, matching `HttpClientResponse.fromWeb`. +Fix repeated `text`, `json`, `arrayBuffer`, and `urlParamsBody` reads in `HttpServerResponse.toClientResponse` for raw Web `Response` bodies, preserving the original response for serving.