From 7b0ac770a15358c9fb302ec48a40b480faf7d9bb Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Tue, 4 Aug 2026 13:53:46 +0000 Subject: [PATCH 1/3] Synchronize response metadata when replacing bodies --- .../test/unstable/http/HttpServerResponse.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/effect/test/unstable/http/HttpServerResponse.test.ts b/packages/effect/test/unstable/http/HttpServerResponse.test.ts index 4fa96734444..5b359805822 100644 --- a/packages/effect/test/unstable/http/HttpServerResponse.test.ts +++ b/packages/effect/test/unstable/http/HttpServerResponse.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest" import { Context, Effect, Stream } from "effect" -import { HttpClientRequest, HttpClientResponse, HttpServerResponse } from "effect/unstable/http" +import { HttpBody, HttpClientRequest, HttpClientResponse, HttpServerResponse } from "effect/unstable/http" const TestValue = Context.Reference("test/TestValue", { defaultValue: () => 0 }) @@ -75,4 +75,13 @@ describe("HttpServerResponse", () => { assert.strictEqual(response.status, 200) assert.strictEqual(yield* roundTrip.text, "") })) + + it("synchronizes body metadata headers for empty and replaced bodies", () => { + const emptyBytes = HttpServerResponse.uint8Array(new Uint8Array()) + assert.strictEqual(emptyBytes.headers["content-length"], "0") + + const replaced = HttpServerResponse.setBody(HttpServerResponse.text("abc"), HttpBody.empty) + assert.notProperty(replaced.headers, "content-type") + assert.notProperty(replaced.headers, "content-length") + }) }) From f417a966d32007dfe0970ada8a904854d798c612 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 22:05:39 +0000 Subject: [PATCH 2/3] Fix response metadata when replacing bodies --- .../fix-http-server-response-body-headers.md | 5 +++++ .../src/unstable/http/HttpServerResponse.ts | 19 ++++++++++++++++--- .../unstable/http/HttpServerResponse.test.ts | 7 +++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-http-server-response-body-headers.md diff --git a/.changeset/fix-http-server-response-body-headers.md b/.changeset/fix-http-server-response-body-headers.md new file mode 100644 index 00000000000..90f22781792 --- /dev/null +++ b/.changeset/fix-http-server-response-body-headers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Synchronize HTTP server response content headers when replacing the body. diff --git a/packages/effect/src/unstable/http/HttpServerResponse.ts b/packages/effect/src/unstable/http/HttpServerResponse.ts index eef398bea67..aa6a631ef88 100644 --- a/packages/effect/src/unstable/http/HttpServerResponse.ts +++ b/packages/effect/src/unstable/http/HttpServerResponse.ts @@ -913,7 +913,20 @@ export const setBody: { (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse } = dual( 2, - (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse => makeResponse({ ...self, body }) + (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse => { + let headers = self.headers + if (body._tag === "Empty" || body._tag === "FormData") { + headers = Headers.remove(Headers.remove(headers, "content-type"), "content-length") + } else { + headers = body.contentType === undefined + ? Headers.remove(headers, "content-type") + : Headers.set(headers, "content-type", body.contentType) + headers = body.contentLength === undefined + ? Headers.remove(headers, "content-length") + : Headers.set(headers, "content-length", body.contentLength.toString()) + } + return makeResponse({ ...self, headers, body }) + } ) /** @@ -1327,13 +1340,13 @@ const makeResponse = (options: { self.body = options.body ?? Body.empty if ( self.body._tag !== "Empty" && - (self.body.contentType || self.body.contentLength) + (self.body.contentType || self.body.contentLength !== undefined) ) { const newHeaders = Headers.fromRecordUnsafe({ ...options.headers }) as any if (self.body.contentType) { newHeaders["content-type"] = self.body.contentType } - if (self.body.contentLength) { + if (self.body.contentLength !== undefined) { newHeaders["content-length"] = self.body.contentLength.toString() } self.headers = newHeaders diff --git a/packages/effect/test/unstable/http/HttpServerResponse.test.ts b/packages/effect/test/unstable/http/HttpServerResponse.test.ts index 5b359805822..fde9f80cc76 100644 --- a/packages/effect/test/unstable/http/HttpServerResponse.test.ts +++ b/packages/effect/test/unstable/http/HttpServerResponse.test.ts @@ -83,5 +83,12 @@ describe("HttpServerResponse", () => { const replaced = HttpServerResponse.setBody(HttpServerResponse.text("abc"), HttpBody.empty) assert.notProperty(replaced.headers, "content-type") assert.notProperty(replaced.headers, "content-length") + + const streamed = HttpServerResponse.setBody( + HttpServerResponse.text("abc"), + HttpBody.stream(Stream.empty, "application/octet-stream") + ) + assert.strictEqual(streamed.headers["content-type"], "application/octet-stream") + assert.notProperty(streamed.headers, "content-length") }) }) From 6130c74dac4fdb0c8b42c2c0d9e3f968b43a32a5 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 22:51:46 +0000 Subject: [PATCH 3/3] Deduplicate HTTP body header synchronization --- .../src/unstable/http/HttpClientRequest.ts | 14 ++------------ .../src/unstable/http/HttpServerResponse.ts | 17 +++-------------- .../src/unstable/http/internal/httpBody.ts | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 26 deletions(-) create mode 100644 packages/effect/src/unstable/http/internal/httpBody.ts diff --git a/packages/effect/src/unstable/http/HttpClientRequest.ts b/packages/effect/src/unstable/http/HttpClientRequest.ts index 2363374e1ef..46751c3b3c0 100644 --- a/packages/effect/src/unstable/http/HttpClientRequest.ts +++ b/packages/effect/src/unstable/http/HttpClientRequest.ts @@ -28,6 +28,7 @@ import * as Stream from "../../Stream.ts" import * as Headers from "./Headers.ts" import * as HttpBody from "./HttpBody.ts" import { hasBody, type HttpMethod } from "./HttpMethod.ts" +import * as bodyInternal from "./internal/httpBody.ts" import * as Url from "./Url.ts" import * as UrlParams from "./UrlParams.ts" @@ -649,23 +650,12 @@ export const setBody: { (body: HttpBody.HttpBody): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest } = dual(2, (self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest => { - let headers = self.headers - if (body._tag === "Empty" || body._tag === "FormData") { - headers = Headers.remove(Headers.remove(headers, "Content-Type"), "Content-length") - } else { - if (body.contentType) { - headers = Headers.set(headers, "content-type", body.contentType) - } - if (body.contentLength !== undefined) { - headers = Headers.set(headers, "content-length", body.contentLength.toString()) - } - } return makeWith( self.method, self.url, self.urlParams, self.hash, - headers, + bodyInternal.updateHeaders(self.headers, body), body ) }) diff --git a/packages/effect/src/unstable/http/HttpServerResponse.ts b/packages/effect/src/unstable/http/HttpServerResponse.ts index aa6a631ef88..960bba53dac 100644 --- a/packages/effect/src/unstable/http/HttpServerResponse.ts +++ b/packages/effect/src/unstable/http/HttpServerResponse.ts @@ -34,6 +34,7 @@ import * as HttpClientRequest from "./HttpClientRequest.ts" import * as HttpClientResponse from "./HttpClientResponse.ts" import * as HttpIncomingMessage from "./HttpIncomingMessage.ts" import type { HttpPlatform } from "./HttpPlatform.ts" +import * as bodyInternal from "./internal/httpBody.ts" import * as Template from "./Template.ts" import * as UrlParams from "./UrlParams.ts" @@ -913,20 +914,8 @@ export const setBody: { (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse } = dual( 2, - (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse => { - let headers = self.headers - if (body._tag === "Empty" || body._tag === "FormData") { - headers = Headers.remove(Headers.remove(headers, "content-type"), "content-length") - } else { - headers = body.contentType === undefined - ? Headers.remove(headers, "content-type") - : Headers.set(headers, "content-type", body.contentType) - headers = body.contentLength === undefined - ? Headers.remove(headers, "content-length") - : Headers.set(headers, "content-length", body.contentLength.toString()) - } - return makeResponse({ ...self, headers, body }) - } + (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse => + makeResponse({ ...self, headers: bodyInternal.updateHeaders(self.headers, body), body }) ) /** diff --git a/packages/effect/src/unstable/http/internal/httpBody.ts b/packages/effect/src/unstable/http/internal/httpBody.ts new file mode 100644 index 00000000000..e033e4415be --- /dev/null +++ b/packages/effect/src/unstable/http/internal/httpBody.ts @@ -0,0 +1,15 @@ +import * as Headers from "../Headers.ts" +import type * as HttpBody from "../HttpBody.ts" + +/** @internal */ +export const updateHeaders = (headers: Headers.Headers, body: HttpBody.HttpBody): Headers.Headers => { + if (body._tag === "Empty" || body._tag === "FormData") { + return Headers.remove(Headers.remove(headers, "content-type"), "content-length") + } + headers = body.contentType === undefined + ? Headers.remove(headers, "content-type") + : Headers.set(headers, "content-type", body.contentType) + return body.contentLength === undefined + ? Headers.remove(headers, "content-length") + : Headers.set(headers, "content-length", body.contentLength.toString()) +}