diff --git a/.changeset/fix-from-web-content-type.md b/.changeset/fix-from-web-content-type.md new file mode 100644 index 00000000000..bfec044c2f4 --- /dev/null +++ b/.changeset/fix-from-web-content-type.md @@ -0,0 +1,7 @@ +--- +"@effect/platform": patch +--- + +HttpServerResponse: fix `fromWeb` to preserve Content-Type header when response has a body + +Previously, when converting a web `Response` to an `HttpServerResponse` via `fromWeb`, the `Content-Type` header was not passed to `Body.stream()`, causing it to default to `application/octet-stream`. This affected any code using `HttpApp.fromWebHandler` to wrap web handlers, as JSON responses would incorrectly have their Content-Type set to `application/octet-stream` instead of `application/json`. diff --git a/packages/platform/src/HttpServerResponse.ts b/packages/platform/src/HttpServerResponse.ts index cd3a5d8e7d9..d5ffa152cc1 100644 --- a/packages/platform/src/HttpServerResponse.ts +++ b/packages/platform/src/HttpServerResponse.ts @@ -410,12 +410,16 @@ export const fromWeb = (response: Response): HttpServerResponse => { cookies: Cookies.fromSetCookie(setCookieHeaders) }) if (response.body) { + const contentType = headers.get("content-type") self = setBody( self, - Body.stream(Stream.fromReadableStream({ - evaluate: () => response.body!, - onError: (e) => e - })) + Body.stream( + Stream.fromReadableStream({ + evaluate: () => response.body!, + onError: (e) => e + }), + contentType ?? undefined + ) ) } return self diff --git a/packages/platform/test/HttpApp.test.ts b/packages/platform/test/HttpApp.test.ts index 7c6602e7c4c..c8291f538c5 100644 --- a/packages/platform/test/HttpApp.test.ts +++ b/packages/platform/test/HttpApp.test.ts @@ -14,6 +14,12 @@ describe("Http/App", () => { }) }) + test("json preserves content-type", async () => { + const handler = HttpApp.toWebHandler(HttpServerResponse.json({ foo: "bar" })) + const response = await handler(new Request("http://localhost:3000/")) + strictEqual(response.headers.get("Content-Type"), "application/json") + }) + test("cookies", async () => { const handler = HttpApp.toWebHandler( HttpServerResponse.unsafeJson({ foo: "bar" }).pipe( @@ -186,5 +192,28 @@ describe("Http/App", () => { const response = await finalHandler(new Request("http://localhost:3000/")) deepStrictEqual(await response.json(), { source: "effect" }) }) + + test("preserves response content-type header", async () => { + const webHandler = async (_request: Request) => { + return Response.json({ message: "hello" }) + } + const app = HttpApp.fromWebHandler(webHandler) + const handler = HttpApp.toWebHandler(app) + const response = await handler(new Request("http://localhost:3000/")) + strictEqual(response.headers.get("Content-Type"), "application/json") + deepStrictEqual(await response.json(), { message: "hello" }) + }) + + test("preserves custom content-type header", async () => { + const webHandler = async (_request: Request) => { + return new Response("", { + headers: { "Content-Type": "text/html; charset=utf-8" } + }) + } + const app = HttpApp.fromWebHandler(webHandler) + const handler = HttpApp.toWebHandler(app) + const response = await handler(new Request("http://localhost:3000/")) + strictEqual(response.headers.get("Content-Type"), "text/html; charset=utf-8") + }) }) })