From 031f2da4742ddd3aefb29802bcdaa36205b5397b Mon Sep 17 00:00:00 2001 From: Taesu Date: Sat, 10 Oct 2026 13:52:31 +0900 Subject: [PATCH] fix(api-key): preserve server errors after validation --- docs/content/docs/plugins/api-key/index.mdx | 12 ++++--- packages/api-key/src/api-key.test.ts | 32 ++++++++++++++++--- packages/api-key/src/routes/verify-api-key.ts | 5 ++- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/docs/content/docs/plugins/api-key/index.mdx b/docs/content/docs/plugins/api-key/index.mdx index 09451bcf9e..90d27e8545 100644 --- a/docs/content/docs/plugins/api-key/index.mdx +++ b/docs/content/docs/plugins/api-key/index.mdx @@ -197,11 +197,13 @@ Otherwise if it throws, it will throw an `APIError`. #### Result -| Outcome | Behavior | -| ------------------------------ | --------------------------------------------------------------------------------------------------------------- | -| The key is accepted | Returns `valid: true`, the key details, and `error: null`. | -| The key is rejected | Returns `valid: false`, an error describing the rejection, and `key: null`. | -| Verification fails to complete | Throws an `APIError`, for example when a database connection fails. Handle this separately from a rejected key. | +`valid: true` means the key passed verification. Failures in best-effort storage updates are logged without changing the verification result. + +| Outcome | Behavior | +| ------------------------------ | --------------------------------------------------------------------------------------- | +| The key is accepted | Returns `valid: true`, the key details, and `error: null`. | +| The key is rejected | Returns `valid: false`, an error describing the rejection, and `key: null`. | +| Verification fails to complete | Throws an `APIError` when a required verification step fails, such as a storage lookup. | ```ts type Result = { diff --git a/packages/api-key/src/api-key.test.ts b/packages/api-key/src/api-key.test.ts index 0538ac184a..2120f19061 100644 --- a/packages/api-key/src/api-key.test.ts +++ b/packages/api-key/src/api-key.test.ts @@ -3,7 +3,11 @@ import type { APIError } from "@better-auth/core/error"; import { isAPIError } from "@better-auth/core/utils/is-api-error"; import { getTestInstance } from "better-auth/test"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { apiKey, API_KEY_ERROR_CODES as ERROR_CODES } from "."; +import { + apiKey, + defaultKeyHasher, + API_KEY_ERROR_CODES as ERROR_CODES, +} from "."; import { apiKeyClient } from "./client"; import type { ApiKey } from "./types"; @@ -5089,16 +5093,36 @@ describe("verify should not write back stale state", async () => { }); }; - const result = await auth.api.verifyApiKey({ - body: { key: created.key }, + await expect( + auth.api.verifyApiKey({ body: { key: created.key } }), + ).rejects.toMatchObject({ + statusCode: 500, + body: { code: "FAILED_TO_UPDATE_API_KEY" }, }); - expect(result.valid).toBe(false); const apiKeyEntries = [...store.keys()].filter((k) => k.startsWith("api-key:"), ); expect(apiKeyEntries).toEqual([]); }); + + it("should report malformed storage data after validation as a verification failure", async () => { + const { headers } = await signInWithTestUser(); + const created = await auth.api.createApiKey({ body: {}, headers }); + const storageKey = `api-key:${await defaultKeyHasher(created.key)}`; + + onValidate = async () => { + store.set(storageKey, "{invalid-json"); + }; + + await expect( + auth.api.verifyApiKey({ body: { key: created.key } }), + ).rejects.toMatchObject({ + statusCode: 500, + body: { code: "FAILED_TO_UPDATE_API_KEY" }, + }); + expect(store.get(storageKey)).toBe("{invalid-json"); + }); }); }); diff --git a/packages/api-key/src/routes/verify-api-key.ts b/packages/api-key/src/routes/verify-api-key.ts index 195c74b334..a812cb20f8 100644 --- a/packages/api-key/src/routes/verify-api-key.ts +++ b/packages/api-key/src/routes/verify-api-key.ts @@ -441,7 +441,10 @@ async function claimUsageInSecondaryStorage({ const updated = await performUpdate(); if (!updated) { - throw APIError.from("UNAUTHORIZED", ERROR_CODES.INVALID_API_KEY); + throw APIError.from( + "INTERNAL_SERVER_ERROR", + ERROR_CODES.FAILED_TO_UPDATE_API_KEY, + ); } return updated; }