From e1d40116e2b6a797372ac82b9feea39f57285632 Mon Sep 17 00:00:00 2001 From: Taesu <166604494+bytaesu@users.noreply.github.com> Date: Tue, 25 Aug 2026 08:37:57 +0000 Subject: [PATCH] fix(logger): respect configured logger in auth request context (#10939) --- .changeset/scoped-logger-cookie-cache.md | 5 ++ .../better-auth/src/cookies/cookies.test.ts | 49 +++++++---- packages/core/src/env/logger.test.ts | 83 ++++++++++++++++++- packages/core/src/env/logger.ts | 23 ++++- 4 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 .changeset/scoped-logger-cookie-cache.md diff --git a/.changeset/scoped-logger-cookie-cache.md b/.changeset/scoped-logger-cookie-cache.md new file mode 100644 index 0000000000..12275a6e67 --- /dev/null +++ b/.changeset/scoped-logger-cookie-cache.md @@ -0,0 +1,5 @@ +--- +"@better-auth/core": patch +--- + +Logs emitted while handling auth requests now respect the configured custom logger, log level, and disabled setting. diff --git a/packages/better-auth/src/cookies/cookies.test.ts b/packages/better-auth/src/cookies/cookies.test.ts index 86a7561ab3..e82078198e 100644 --- a/packages/better-auth/src/cookies/cookies.test.ts +++ b/packages/better-auth/src/cookies/cookies.test.ts @@ -1,5 +1,7 @@ import type { BetterAuthOptions } from "@better-auth/core"; -import { logger } from "@better-auth/core/env"; +import type { AuthEndpointContext } from "@better-auth/core/context"; +import { runWithEndpointContext } from "@better-auth/core/context"; +import { createLogger } from "@better-auth/core/env"; import type { GoogleProfile } from "@better-auth/core/social-providers"; import { safeJSONParse } from "@better-auth/core/utils/json"; import { base64Url } from "@better-auth/utils/base64"; @@ -15,6 +17,7 @@ import { describe, expect, it, + onTestFinished, vi, } from "vitest"; import { @@ -1274,7 +1277,7 @@ describe("Cookie Cache Field Filtering", () => { /** * @see https://github.com/better-auth/better-auth/issues/10902 */ - it("should warn when a signed compact cookie has an invalid payload", async () => { + it("should use the configured logger for an invalid signed compact cookie", async () => { const secret = "test-secret"; const bakeCookie = async (emailVerified: boolean | null) => { const now = new Date().toISOString(); @@ -1313,25 +1316,35 @@ describe("Cookie Cache Field Filtering", () => { isSecure: false, secret, }; - const warn = vi.spyOn(logger, "warn").mockImplementation(() => {}); - try { - const validCache = await getCookieCache( - new Headers({ cookie: `p.session_data=${await bakeCookie(false)}` }), - config, - ); - expect(validCache).not.toBeNull(); - expect(warn).not.toHaveBeenCalled(); + const log = vi.fn(); + const defaultWarn = vi.spyOn(console, "warn").mockImplementation(() => {}); + onTestFinished(() => defaultWarn.mockRestore()); + const endpointContext = { + context: { logger: createLogger({ log }) }, + } as unknown as AuthEndpointContext; + const validCookie = await bakeCookie(false); - const invalidCache = await getCookieCache( - new Headers({ cookie: `p.session_data=${await bakeCookie(null)}` }), + const validCache = await runWithEndpointContext(endpointContext, () => + getCookieCache( + new Headers({ cookie: `p.session_data=${validCookie}` }), config, - ); + ), + ); + expect(validCache).not.toBeNull(); + expect(log).not.toHaveBeenCalled(); + const invalidCookie = await bakeCookie(null); - expect(invalidCache).toBeNull(); - expect(warn).toHaveBeenCalledTimes(1); - } finally { - warn.mockRestore(); - } + const invalidCache = await runWithEndpointContext(endpointContext, () => + getCookieCache( + new Headers({ cookie: `p.session_data=${invalidCookie}` }), + config, + ), + ); + + expect(invalidCache).toBeNull(); + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls[0]?.[0]).toBe("warn"); + expect(defaultWarn).not.toHaveBeenCalled(); }); it("should return null for invalid JWT token", async () => { diff --git a/packages/core/src/env/logger.test.ts b/packages/core/src/env/logger.test.ts index 05c89be41d..0020664464 100644 --- a/packages/core/src/env/logger.test.ts +++ b/packages/core/src/env/logger.test.ts @@ -1,6 +1,9 @@ -import { describe, expect, it } from "vitest"; -import type { LogLevel } from "./logger"; -import { shouldPublishLog } from "./logger"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; +import type { AuthEndpointContext } from "../context"; +import { runWithEndpointContext } from "../context"; +import { safeJSONParse } from "../utils/json"; +import type { InternalLogger, LogLevel } from "./logger"; +import { createLogger, logger, shouldPublishLog } from "./logger"; describe("shouldPublishLog", () => { const testCases: { @@ -32,3 +35,77 @@ describe("shouldPublishLog", () => { }); }); }); + +describe("logger", () => { + const endpointContext = (logger: InternalLogger) => + ({ context: { logger } }) as unknown as AuthEndpointContext; + + it("uses the default logger outside an auth context", () => { + const defaultWarn = vi.spyOn(console, "warn").mockImplementation(() => {}); + onTestFinished(() => defaultWarn.mockRestore()); + + logger.warn("default warning"); + + expect(defaultWarn).toHaveBeenCalledTimes(1); + }); + + it("delegates to the logger in the current auth context", async () => { + const defaultWarn = vi.spyOn(console, "warn").mockImplementation(() => {}); + onTestFinished(() => defaultWarn.mockRestore()); + const firstLog = vi.fn(); + const secondLog = vi.fn(); + const firstLogger = createLogger({ level: "info", log: firstLog }); + const secondLogger = createLogger({ level: "error", log: secondLog }); + + await Promise.all([ + runWithEndpointContext(endpointContext(firstLogger), async () => { + await Promise.resolve(); + expect(logger.level).toBe("info"); + logger.warn("first warning"); + }), + runWithEndpointContext(endpointContext(secondLogger), async () => { + await Promise.resolve(); + expect(logger.level).toBe("error"); + logger.warn("second warning"); + }), + ]); + + expect(firstLog).toHaveBeenCalledWith("warn", "first warning"); + expect(secondLog).not.toHaveBeenCalled(); + expect(defaultWarn).not.toHaveBeenCalled(); + }); + + it("routes utility logs through the current auth context", async () => { + const defaultError = vi + .spyOn(console, "error") + .mockImplementation(() => {}); + onTestFinished(() => defaultError.mockRestore()); + const log = vi.fn(); + const configuredLogger = createLogger({ log }); + + await runWithEndpointContext(endpointContext(configuredLogger), () => { + expect(safeJSONParse("{")).toBeNull(); + }); + + expect(log).toHaveBeenCalledWith( + "error", + "Error parsing JSON", + expect.objectContaining({ error: expect.any(SyntaxError) }), + ); + expect(defaultError).not.toHaveBeenCalled(); + }); + + it("honors disabled logging in the current auth context", async () => { + const defaultWarn = vi.spyOn(console, "warn").mockImplementation(() => {}); + onTestFinished(() => defaultWarn.mockRestore()); + const log = vi.fn(); + const configuredLogger = createLogger({ disabled: true, log }); + + await runWithEndpointContext(endpointContext(configuredLogger), () => { + logger.warn("hidden warning"); + }); + + expect(log).not.toHaveBeenCalled(); + expect(defaultWarn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/env/logger.ts b/packages/core/src/env/logger.ts index fc83682377..476cb601ce 100644 --- a/packages/core/src/env/logger.ts +++ b/packages/core/src/env/logger.ts @@ -1,3 +1,5 @@ +import type { AuthEndpointContext } from "../context/endpoint-context"; +import { __getCurrentEndpointContext } from "../context/global"; import { getColorDepth } from "./color-depth"; export const TTY_COLORS = { @@ -142,4 +144,23 @@ export const createLogger = (options?: Logger | undefined): InternalLogger => { }; }; -export const logger = createLogger(); +const defaultLogger = createLogger(); + +const getCurrentLogger = (): InternalLogger => { + const currentLogger = + __getCurrentEndpointContext()?.context.logger; + return currentLogger && currentLogger !== logger + ? currentLogger + : defaultLogger; +}; + +export const logger: InternalLogger = { + debug: (...params) => getCurrentLogger().debug(...params), + info: (...params) => getCurrentLogger().info(...params), + success: (...params) => getCurrentLogger().success(...params), + warn: (...params) => getCurrentLogger().warn(...params), + error: (...params) => getCurrentLogger().error(...params), + get level() { + return getCurrentLogger().level; + }, +};