Skip to content

Commit 00715a6

Browse files
chore(ui,shared,localizations): Improve error handling when creating API keys (#8056)
Co-authored-by: Jacek Radko <jacek@clerk.dev>
1 parent 7c7d025 commit 00715a6

5 files changed

Lines changed: 101 additions & 5 deletions

File tree

.changeset/cyan-elephants-roll.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@clerk/localizations": patch
3+
"@clerk/shared": patch
4+
"@clerk/ui": patch
5+
---
6+
7+
Improved error handling when creating API keys.

integration/tests/api-keys-component.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,4 +757,78 @@ test.describe('api keys component @machine', () => {
757757
}
758758
});
759759
});
760+
761+
test('shows error when creating API key with duplicate name', async ({ page, context }) => {
762+
const u = createTestUtils({ app, page, context });
763+
await u.po.signIn.goTo();
764+
await u.po.signIn.waitForMounted();
765+
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
766+
await u.po.expect.toBeSignedIn();
767+
768+
await u.po.page.goToRelative('/api-keys');
769+
await u.po.apiKeys.waitForMounted();
770+
771+
const duplicateName = `${fakeAdmin.firstName}-duplicate-${Date.now()}`;
772+
773+
// Create the first API key
774+
await u.po.apiKeys.clickAddButton();
775+
await u.po.apiKeys.waitForFormOpened();
776+
await u.po.apiKeys.typeName(duplicateName);
777+
await u.po.apiKeys.selectExpiration('1d');
778+
await u.po.apiKeys.clickSaveButton();
779+
780+
await u.po.apiKeys.waitForCopyModalOpened();
781+
await u.po.apiKeys.clickCopyAndCloseButton();
782+
await u.po.apiKeys.waitForCopyModalClosed();
783+
await u.po.apiKeys.waitForFormClosed();
784+
785+
// Try to create another API key with the same name
786+
await u.po.apiKeys.clickAddButton();
787+
await u.po.apiKeys.waitForFormOpened();
788+
await u.po.apiKeys.typeName(duplicateName);
789+
await u.po.apiKeys.selectExpiration('1d');
790+
await u.po.apiKeys.clickSaveButton();
791+
792+
// Verify error message is displayed
793+
await expect(u.page.getByText('API Key name already exists.')).toBeVisible({ timeout: 5000 });
794+
});
795+
796+
test('shows error when API key usage is exceeded for free plan', async ({ page, context }) => {
797+
const u = createTestUtils({ app, page, context });
798+
await u.po.signIn.goTo();
799+
await u.po.signIn.waitForMounted();
800+
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
801+
await u.po.expect.toBeSignedIn();
802+
803+
// Mock the API keys create endpoint to return 403 for free plan users who exceed free tier limits
804+
await page.route('*/**/api_keys*', async route => {
805+
if (route.request().method() === 'POST') {
806+
await route.fulfill({
807+
status: 403,
808+
contentType: 'application/json',
809+
body: JSON.stringify({
810+
errors: [{ code: 'token_quota_exceeded', message: 'Token quota exceeded' }],
811+
}),
812+
});
813+
} else {
814+
await route.continue();
815+
}
816+
});
817+
818+
await u.po.page.goToRelative('/api-keys');
819+
await u.po.apiKeys.waitForMounted();
820+
821+
await u.po.apiKeys.clickAddButton();
822+
await u.po.apiKeys.waitForFormOpened();
823+
await u.po.apiKeys.typeName(`${fakeAdmin.firstName}-test-usage-exceeded`);
824+
await u.po.apiKeys.selectExpiration('1d');
825+
await u.po.apiKeys.clickSaveButton();
826+
827+
// Verify error message is displayed
828+
await expect(
829+
u.page.getByText('You have reached your usage limit. You can remove the limit by upgrading to a paid plan.'),
830+
).toBeVisible({ timeout: 5000 });
831+
832+
await u.page.unrouteAll();
833+
});
760834
});

packages/localizations/src/en-US.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,8 @@ export const enUS: LocalizationResource = {
975975
},
976976
unstable__errors: {
977977
already_a_member_in_organization: '{{email}} is already a member of the organization.',
978+
api_key_name_already_exists: 'API Key name already exists.',
979+
api_key_usage_exceeded: 'You have reached your usage limit. You can remove the limit by upgrading to a paid plan.',
978980
avatar_file_size_exceeded: 'File size exceeds the maximum limit of 10MB. Please choose a smaller file.',
979981
avatar_file_type_invalid: 'File type not supported. Please upload a JPG, PNG, GIF, or WEBP image.',
980982
captcha_invalid: undefined,

packages/shared/src/types/localization.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,6 +1508,8 @@ type UnstableErrors = WithParamName<{
15081508
organization_domain_common: LocalizationValue;
15091509
organization_domain_blocked: LocalizationValue;
15101510
organization_domain_exists_for_enterprise_connection: LocalizationValue;
1511+
api_key_name_already_exists: LocalizationValue;
1512+
api_key_usage_exceeded: LocalizationValue;
15111513
organization_membership_quota_exceeded: LocalizationValue;
15121514
organization_not_found_or_unauthorized: LocalizationValue;
15131515
organization_not_found_or_unauthorized_with_create_organization_disabled: LocalizationValue;

packages/ui/src/components/APIKeys/APIKeys.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
2727
import { InputWithIcon } from '@/ui/elements/InputWithIcon';
2828
import { Pagination } from '@/ui/elements/Pagination';
2929
import { useDebounce } from '@/ui/hooks';
30+
import { handleError } from '@/ui/utils/errorHandler';
3031
import { MagnifyingGlass } from '@/ui/icons';
3132
import { mqu } from '@/ui/styledSystem';
3233

@@ -114,15 +115,25 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr
114115
...params,
115116
subject,
116117
});
117-
invalidateAll();
118+
void invalidateAll();
118119
card.setError(undefined);
119120
setIsCopyModalOpen(true);
120121
setAPIKey(apiKey);
121122
} catch (err: any) {
122-
if (isClerkAPIResponseError(err)) {
123-
if (err.status === 409) {
124-
card.setError('API Key name already exists');
125-
}
123+
if (!isClerkAPIResponseError(err)) {
124+
handleError(err, [], card.setError);
125+
return;
126+
}
127+
128+
switch (err.errors?.[0]?.code) {
129+
case 'token_quota_exceeded':
130+
card.setError(t(localizationKeys('unstable__errors.api_key_usage_exceeded')));
131+
break;
132+
case 'token_creation_conflict':
133+
card.setError(t(localizationKeys('unstable__errors.api_key_name_already_exists')));
134+
break;
135+
default:
136+
handleError(err, [], card.setError);
126137
}
127138
} finally {
128139
card.setIdle();

0 commit comments

Comments
 (0)