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"}'); }); });