From 97800c6cba830134314eaffac43f0b08760ce1a1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 12 Mar 2026 13:37:55 -0700 Subject: [PATCH 1/6] chore: Improve error handling when creating API keys --- .../tests/machine-auth/component.test.ts | 39 +++++++++++++++++++ packages/localizations/src/en-US.ts | 2 + packages/shared/src/types/localization.ts | 2 + .../ui/src/components/APIKeys/APIKeys.tsx | 8 ++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index 989abe8145a..f5772b90c88 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -757,4 +757,43 @@ test.describe('api keys component @machine', () => { } }); }); + + test('shows error when API key usage is exceeded for free plan', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password }); + await u.po.expect.toBeSignedIn(); + + // Mock the API keys create endpoint to return 403 for free plan users who exceed free tier limits + await page.route('*/**/v1/api_keys', async route => { + if (route.request().method() === 'POST') { + await route.fulfill({ + status: 403, + contentType: 'application/json', + body: JSON.stringify({ + errors: [{ code: 'token_quota_exceeded', message: 'Token quota exceeded' }], + }), + }); + } else { + await route.continue(); + } + }); + + await u.po.page.goToRelative('/api-keys'); + await u.po.apiKeys.waitForMounted(); + + await u.po.apiKeys.clickAddButton(); + await u.po.apiKeys.waitForFormOpened(); + await u.po.apiKeys.typeName('test-key'); + await u.po.apiKeys.selectExpiration('1d'); + await u.po.apiKeys.clickSaveButton(); + + // Verify error message is displayed + await expect( + u.page.getByText('You have reached your usage limit. You can remove the limit by upgrading to a paid plan.'), + ).toBeVisible({ timeout: 5000 }); + + await u.page.unrouteAll(); + }); }); diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index cd8b0200914..d46dcc63891 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -975,6 +975,8 @@ export const enUS: LocalizationResource = { }, unstable__errors: { already_a_member_in_organization: '{{email}} is already a member of the organization.', + api_key_name_already_exists: 'API Key name already exists.', + api_key_usage_exceeded: 'You have reached your usage limit. You can remove the limit by upgrading to a paid plan.', avatar_file_size_exceeded: 'File size exceeds the maximum limit of 10MB. Please choose a smaller file.', avatar_file_type_invalid: 'File type not supported. Please upload a JPG, PNG, GIF, or WEBP image.', captcha_invalid: undefined, diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 63ad4de0f21..586dde32982 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1507,6 +1507,8 @@ type UnstableErrors = WithParamName<{ organization_domain_common: LocalizationValue; organization_domain_blocked: LocalizationValue; organization_domain_exists_for_enterprise_connection: LocalizationValue; + api_key_name_already_exists: LocalizationValue; + api_key_usage_exceeded: LocalizationValue; organization_membership_quota_exceeded: LocalizationValue; organization_not_found_or_unauthorized: LocalizationValue; organization_not_found_or_unauthorized_with_create_organization_disabled: LocalizationValue; diff --git a/packages/ui/src/components/APIKeys/APIKeys.tsx b/packages/ui/src/components/APIKeys/APIKeys.tsx index f137abff203..da69c375e40 100644 --- a/packages/ui/src/components/APIKeys/APIKeys.tsx +++ b/packages/ui/src/components/APIKeys/APIKeys.tsx @@ -114,14 +114,16 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr ...params, subject, }); - invalidateAll(); + void invalidateAll(); card.setError(undefined); setIsCopyModalOpen(true); setAPIKey(apiKey); } catch (err: any) { if (isClerkAPIResponseError(err)) { - if (err.status === 409) { - card.setError('API Key name already exists'); + if (err.status === 403) { + card.setError(t(localizationKeys('unstable__errors.api_key_usage_exceeded'))); + } else if (err.status === 409) { + card.setError(t(localizationKeys('unstable__errors.api_key_name_already_exists'))); } } } finally { From 5b7dfa9ec9073ead8ea93b818256ff9c77855b1a Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Thu, 12 Mar 2026 13:56:53 -0700 Subject: [PATCH 2/6] chore: add changeset --- .changeset/cyan-elephants-roll.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/cyan-elephants-roll.md diff --git a/.changeset/cyan-elephants-roll.md b/.changeset/cyan-elephants-roll.md new file mode 100644 index 00000000000..c3cb57aa0d0 --- /dev/null +++ b/.changeset/cyan-elephants-roll.md @@ -0,0 +1,7 @@ +--- +"@clerk/localizations": patch +"@clerk/shared": patch +"@clerk/ui": patch +--- + +Improved error handling when creating API keys. From 31ba564ee895ea7ddfc2f35c923a33564d98ca1a Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 12 Mar 2026 14:12:58 -0700 Subject: [PATCH 3/6] chore: remove versioning from mock API keys POST route --- integration/tests/machine-auth/component.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index f5772b90c88..2bb04f9f147 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -766,7 +766,7 @@ test.describe('api keys component @machine', () => { await u.po.expect.toBeSignedIn(); // Mock the API keys create endpoint to return 403 for free plan users who exceed free tier limits - await page.route('*/**/v1/api_keys', async route => { + await page.route('*/**/api_keys', async route => { if (route.request().method() === 'POST') { await route.fulfill({ status: 403, From 6579336f9041cac2176b29c45ecda0b8a6123d3b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 12 Mar 2026 14:15:31 -0700 Subject: [PATCH 4/6] chore: more test fixes --- integration/tests/machine-auth/component.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index 2bb04f9f147..e30d3c3c2cf 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -785,7 +785,7 @@ test.describe('api keys component @machine', () => { await u.po.apiKeys.clickAddButton(); await u.po.apiKeys.waitForFormOpened(); - await u.po.apiKeys.typeName('test-key'); + await u.po.apiKeys.typeName(`${fakeAdmin.firstName}-test-usage-exceeded`); await u.po.apiKeys.selectExpiration('1d'); await u.po.apiKeys.clickSaveButton(); From 70412711c9df1f955a7297e743bebc949e9b0012 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 12 Mar 2026 14:38:02 -0700 Subject: [PATCH 5/6] chore: retry --- integration/tests/machine-auth/component.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index e30d3c3c2cf..90a779ffe9e 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -766,7 +766,7 @@ test.describe('api keys component @machine', () => { await u.po.expect.toBeSignedIn(); // Mock the API keys create endpoint to return 403 for free plan users who exceed free tier limits - await page.route('*/**/api_keys', async route => { + await page.route('*/**/api_keys*', async route => { if (route.request().method() === 'POST') { await route.fulfill({ status: 403, From 9dcc47cc6a6fded236bbec7e223640372dc4280f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 13 Mar 2026 08:33:35 -0700 Subject: [PATCH 6/6] chore: add duplicate api key name test --- .../tests/machine-auth/component.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index 90a779ffe9e..0712a8e32c2 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -758,6 +758,41 @@ test.describe('api keys component @machine', () => { }); }); + test('shows error when creating API key with duplicate name', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password }); + await u.po.expect.toBeSignedIn(); + + await u.po.page.goToRelative('/api-keys'); + await u.po.apiKeys.waitForMounted(); + + const duplicateName = `${fakeAdmin.firstName}-duplicate-${Date.now()}`; + + // Create the first API key + await u.po.apiKeys.clickAddButton(); + await u.po.apiKeys.waitForFormOpened(); + await u.po.apiKeys.typeName(duplicateName); + await u.po.apiKeys.selectExpiration('1d'); + await u.po.apiKeys.clickSaveButton(); + + await u.po.apiKeys.waitForCopyModalOpened(); + await u.po.apiKeys.clickCopyAndCloseButton(); + await u.po.apiKeys.waitForCopyModalClosed(); + await u.po.apiKeys.waitForFormClosed(); + + // Try to create another API key with the same name + await u.po.apiKeys.clickAddButton(); + await u.po.apiKeys.waitForFormOpened(); + await u.po.apiKeys.typeName(duplicateName); + await u.po.apiKeys.selectExpiration('1d'); + await u.po.apiKeys.clickSaveButton(); + + // Verify error message is displayed + await expect(u.page.getByText('API Key name already exists.')).toBeVisible({ timeout: 5000 }); + }); + test('shows error when API key usage is exceeded for free plan', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.po.signIn.goTo();