From 9785eb221fb77ce3e6d24c6e79da2beccb7f80ea Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Thu, 15 Oct 2026 05:10:51 +0200 Subject: [PATCH] fix(auth): Anmeldung unter Node 24 (Proxy um NextRequest) Next reicht Route-Handlern einen Proxy um NextRequest. undici 7 (Node 24) prueft private Felder (#state), die ein Proxy nicht besitzt; new Request(request) warf "Cannot read private member #state" und jede /api/auth-Anfrage endete lokal mit 500. Produktion (Node 22, undici 6) war nicht betroffen. withClientIp baut die Anfrage jetzt aus URL, Methode, Headern und Body neu. Regressionstest mit einem Proxy wie in Next. --- src/app/api/auth/[...all]/route.ts | 4 ++-- src/lib/rate-limit.ts | 14 +++++++++++--- tests/client-ip.test.ts | 28 ++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts index face8f3..71a84fc 100644 --- a/src/app/api/auth/[...all]/route.ts +++ b/src/app/api/auth/[...all]/route.ts @@ -3,9 +3,9 @@ import { getAuth } from "@/lib/auth"; import { withClientIp } from "@/lib/rate-limit"; export async function GET(request: Request) { - return toNextJsHandler(getAuth()).GET(withClientIp(request)); + return toNextJsHandler(getAuth()).GET(await withClientIp(request)); } export async function POST(request: Request) { - return toNextJsHandler(getAuth()).POST(withClientIp(request)); + return toNextJsHandler(getAuth()).POST(await withClientIp(request)); } diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts index 2cfcb68..5588d88 100644 --- a/src/lib/rate-limit.ts +++ b/src/lib/rate-limit.ts @@ -147,7 +147,15 @@ export function headersWithClientIp(source: Headers): Headers { return headers; } -/** Kopie der Anfrage mit CLIENT_IP_HEADER aus clientIp(); ein mitgeschickter Wert wird ersetzt. */ -export function withClientIp(request: Request): Request { - return new Request(request, { headers: headersWithClientIp(request.headers) }); +/** + * Kopie der Anfrage mit CLIENT_IP_HEADER aus clientIp(); ein mitgeschickter Wert wird ersetzt. + * + * Bewusst aus URL, Methode und Body neu gebaut statt `new Request(request, …)`: + * Next reicht Route-Handlern einen Proxy um NextRequest, und undici 7 (Node 24) + * prueft private Felder (#state), die ein Proxy nicht hat ("Cannot read private + * member #state"). Unter Node 22 (undici 6, Symbole) fiel das nicht auf. + */ +export async function withClientIp(request: Request): Promise { + const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.arrayBuffer(); + return new Request(request.url, { method: request.method, headers: headersWithClientIp(request.headers), body, signal: request.signal }); } diff --git a/tests/client-ip.test.ts b/tests/client-ip.test.ts index a844c6b..8940227 100644 --- a/tests/client-ip.test.ts +++ b/tests/client-ip.test.ts @@ -28,18 +28,38 @@ describe("clientIp hinter Traefik", () => { expect(rateLimitSubject("198.51.100.9")).toBe("198.51.100.9"); }); - test("better-auth bekommt eine einzelne gültige IP (es normalisiert IPv6 selbst auf /64)", () => { + test("better-auth bekommt eine einzelne gültige IP (es normalisiert IPv6 selbst auf /64)", async () => { const request = new Request("https://example.org/api/auth/sign-in/email", { headers: { "x-forwarded-for": "2001:db8:1:2:aaaa::1" } }); - expect(withClientIp(request).headers.get(CLIENT_IP_HEADER)).toBe("2001:db8:1:2:aaaa::1"); + expect((await withClientIp(request)).headers.get(CLIENT_IP_HEADER)).toBe("2001:db8:1:2:aaaa::1"); }); - test("withClientIp setzt den Header für better-auth und überschreibt einen mitgeschickten Wert", () => { + test("withClientIp setzt den Header für better-auth und überschreibt einen mitgeschickten Wert", async () => { const request = new Request("https://example.org/api/auth/sign-in/email", { method: "POST", headers: { "x-forwarded-for": "1.2.3.4, 198.51.100.9", [CLIENT_IP_HEADER]: "1.2.3.4" }, body: "{}", }); - expect(withClientIp(request).headers.get(CLIENT_IP_HEADER)).toBe("198.51.100.9"); + expect((await withClientIp(request)).headers.get(CLIENT_IP_HEADER)).toBe("198.51.100.9"); + }); + + test("withClientIp verträgt Nexts Proxy um NextRequest (Node 24, undici 7) und behält Methode und Body", async () => { + // Next 16 reicht Route-Handlern einen Proxy, dessen Methoden an das Original gebunden sind. + // undici 7 prüft private Felder (#state); ein Proxy besteht diese Prüfung nicht. + const original = new Request("https://example.org/api/auth/sign-in/email", { + method: "POST", + headers: { "x-forwarded-for": "198.51.100.9", "content-type": "application/json" }, + body: '{"email":"a@example.org"}', + }); + const wieNext = new Proxy(original, { + get(target, key) { + const wert = Reflect.get(target, key, target); + return typeof wert === "function" ? wert.bind(target) : wert; + }, + }); + const kopie = await withClientIp(wieNext); + expect(kopie.method).toBe("POST"); + expect(kopie.headers.get(CLIENT_IP_HEADER)).toBe("198.51.100.9"); + expect(await kopie.text()).toBe('{"email":"a@example.org"}'); }); });