diff --git a/packages/auth/src/better-auth.ts b/packages/auth/src/better-auth.ts index aec086f..5995cc2 100644 --- a/packages/auth/src/better-auth.ts +++ b/packages/auth/src/better-auth.ts @@ -6,7 +6,7 @@ import { type DiagnosticOptions, } from 'pgstencil/diagnostics'; import { betterAuth, type BetterAuthOptions } from 'better-auth'; -import { getSessionFromCtx } from 'better-auth/api'; +import { getSessionFromCtx, isAPIError } from 'better-auth/api'; import { lastLoginMethod } from 'better-auth/plugins'; import { emailOTP } from 'better-auth/plugins/email-otp'; import { Hono } from 'hono'; @@ -83,6 +83,8 @@ export function authOptions(options: AuthOptions): BetterAuthOptions { onAPIError: { errorURL: options.origin + (options.errorPath ?? '/'), onError: (error) => { + // Expected 4xx responses are recorded by auth.rejected after dispatch. + if (isAPIError(error) && error.statusCode < 500) return; diagnostic('request.failed', { reason: 'unexpected_error', ...diagnosticError(error), diff --git a/tests/integration/better-auth-oauth.test.ts b/tests/integration/better-auth-oauth.test.ts index badc19f..e719b3b 100644 --- a/tests/integration/better-auth-oauth.test.ts +++ b/tests/integration/better-auth-oauth.test.ts @@ -1026,5 +1026,14 @@ test('auth diagnostics identify Microsoft token failures without recording crede expect.objectContaining({ event: 'email.delivery.succeeded' }), ]), ); + const before = records.length; + const rejected = await next.post('sign-in/email-otp', { + email: 'private-email@example.test', + otp: '00000000', + }); + expect(rejected.status).toBeGreaterThanOrEqual(400); + const rejectionLogs = records.slice(before); + expect(rejectionLogs.some((r) => r.event === 'auth.rejected')).toBe(true); + expect(rejectionLogs.some((r) => r.event === 'request.failed')).toBe(false); expect(JSON.stringify(records)).not.toContain('private-email'); });