From 0cbdf5fb2db245370b8a53b93065cc459677e43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C5=A1a=20=C5=A0ijak?= Date: Mon, 9 Mar 2026 11:10:02 +0100 Subject: [PATCH] fix(better-auth): preserve response headers for early before hook returns --- .../src/api/to-auth-endpoints.test.ts | 51 +++++++++++++++++++ .../better-auth/src/api/to-auth-endpoints.ts | 26 +++++++--- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/packages/better-auth/src/api/to-auth-endpoints.test.ts b/packages/better-auth/src/api/to-auth-endpoints.test.ts index 517bc7fac4..824baa0d81 100644 --- a/packages/better-auth/src/api/to-auth-endpoints.test.ts +++ b/packages/better-auth/src/api/to-auth-endpoints.test.ts @@ -199,6 +199,15 @@ describe("before hook", async () => { return { response: true }; }, ), + responseHeaders: createAuthEndpoint( + "/response-headers", + { + method: "POST", + }, + async (c) => { + return { response: true }; + }, + ), }; const authContext = init({ @@ -207,6 +216,15 @@ describe("before hook", async () => { if (c.path === "/json") { return { before: true }; } + if (c.path === "/response-headers") { + return new Response(JSON.stringify({ before: true }), { + status: 201, + headers: { + "content-type": "application/json", + "x-hook": "before", + }, + }); + } return new Response(JSON.stringify({ before: true })); }), }, @@ -222,6 +240,39 @@ describe("before hook", async () => { const response = await authEndpoints.json(); expect(response).toMatchObject({ before: true }); }); + + it("should not leak request headers into early before responses", async () => { + const response = await authEndpoints.responseHeaders({ + asResponse: true, + headers: new Headers({ + "content-length": "999", + "x-request": "leak-me-not", + }), + }); + + expect(response.status).toBe(201); + expect(response.headers.get("x-hook")).toBe("before"); + expect(response.headers.get("x-request")).toBeNull(); + expect(response.headers.get("content-length")).toBeNull(); + await expect(response.json()).resolves.toMatchObject({ before: true }); + }); + + it("should return response headers and status for early before responses", async () => { + const result = await authEndpoints.responseHeaders({ + returnHeaders: true, + returnStatus: true, + headers: new Headers({ + "content-length": "999", + "x-request": "leak-me-not", + }), + }); + + expect(result.status).toBe(201); + expect(result.headers.get("x-hook")).toBe("before"); + expect(result.headers.get("x-request")).toBeNull(); + expect(result.headers.get("content-length")).toBeNull(); + expect(result.response).toBeInstanceOf(Response); + }); }); }); diff --git a/packages/better-auth/src/api/to-auth-endpoints.ts b/packages/better-auth/src/api/to-auth-endpoints.ts index e9b64d426d..2735d7eb38 100644 --- a/packages/better-auth/src/api/to-auth-endpoints.ts +++ b/packages/better-auth/src/api/to-auth-endpoints.ts @@ -160,16 +160,28 @@ export function toAuthEndpoints>( internalContext = defuReplaceArrays(rest, internalContext); } else if (before) { /* Return before hook response if it's anything other than a context return */ - return context?.asResponse - ? toResponse(before, { - headers: context?.headers, - }) - : context?.returnHeaders + const response = toResponse(before); + if (context?.asResponse) { + return response; + } + if (context?.returnHeaders) { + return context?.returnStatus ? { - headers: context?.headers, + headers: response.headers, response: before, + status: response.status, } - : before; + : { + headers: response.headers, + response: before, + }; + } + return context?.returnStatus + ? { + response: before, + status: response.status, + } + : before; } internalContext.asResponse = false;