From 8f00fc4d0d1b23f087148db9eadf2b1d99dad818 Mon Sep 17 00:00:00 2001 From: Devin Gould Date: Tue, 18 Aug 2026 09:47:07 -0700 Subject: [PATCH] feat(nextjs): point missing-key errors at the Clerk CLI Co-Authored-By: Claude Fable 5 --- .changeset/missing-key-cli-init.md | 6 ++++++ .../src/__tests__/createRedirect.test.ts | 4 ++-- .../__tests__/clerkMiddlewareKeyless.test.ts | 15 ++++++++----- packages/nextjs/src/server/clerkMiddleware.ts | 21 ++++++++++++------- packages/nextjs/src/server/errors.ts | 8 +++++++ packages/shared/src/__tests__/error.spec.ts | 2 +- .../src/__tests__/loadClerkJsScript.spec.ts | 4 ++-- packages/shared/src/errors/errorThrower.ts | 12 +++++++++-- 8 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 .changeset/missing-key-cli-init.md diff --git a/.changeset/missing-key-cli-init.md b/.changeset/missing-key-cli-init.md new file mode 100644 index 00000000000..470c3798d21 --- /dev/null +++ b/.changeset/missing-key-cli-init.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': patch +'@clerk/nextjs': patch +--- + +Update missing key error messages to recommend the Clerk CLI: `npx clerk@latest init` for setup, and `npx clerk@latest deploy` / `npx clerk@latest env pull --instance prod` when keys are missing in production Next.js environments. diff --git a/packages/backend/src/__tests__/createRedirect.test.ts b/packages/backend/src/__tests__/createRedirect.test.ts index 0877146bb89..659d7445d46 100644 --- a/packages/backend/src/__tests__/createRedirect.test.ts +++ b/packages/backend/src/__tests__/createRedirect.test.ts @@ -28,7 +28,7 @@ describe('redirect(redirectAdapter)', () => { } as any); expect(() => redirectToSignIn({ returnBackUrl })).toThrowError( - '@clerk/backend: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/backend: Missing publishableKey. To set up Clerk for this project, run:', ); }); }); @@ -258,7 +258,7 @@ describe('redirect(redirectAdapter)', () => { }); expect(() => redirectToSignUp({ returnBackUrl })).toThrowError( - '@clerk/backend: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/backend: Missing publishableKey. To set up Clerk for this project, run:', ); }); diff --git a/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts index cce1ef319b7..425b66f1654 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts @@ -46,15 +46,20 @@ describe('clerkMiddleware when Clerk env vars are missing', () => { await expect(runMiddleware({ authorization: 'Bearer mt_xxxxxxxx' })).rejects.toThrow(/npx clerk@latest init/); }); - it('falls back to the standard missing-key error when keyless is unavailable', async () => { + it('throws the deploy error pointing at the CLI in production', async () => { vi.stubEnv('NODE_ENV', 'production'); - await expect(runMiddleware()).rejects.toThrow(/publishableKey/i); + await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest deploy/); + await expect(runMiddleware()).rejects.toThrow(/\(code=missing_env_keys_production\)/); }); it('names both env vars and the CLI command in the message', async () => { - const { keylessMissingEnvVars } = await import('../errors.js'); - expect(keylessMissingEnvVars).toContain('NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'); - expect(keylessMissingEnvVars).toContain('CLERK_SECRET_KEY'); + const { keylessMissingEnvVars, productionMissingEnvVars } = await import('../errors.js'); + for (const message of [keylessMissingEnvVars, productionMissingEnvVars]) { + expect(message).toContain('NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'); + expect(message).toContain('CLERK_SECRET_KEY'); + } expect(keylessMissingEnvVars).toContain('npx clerk@latest init'); + expect(productionMissingEnvVars).toContain('npx clerk@latest deploy'); + expect(productionMissingEnvVars).toContain('npx clerk@latest env pull --instance prod'); }); }); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 182617648e6..866d4ea95e5 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -24,6 +24,7 @@ import { isProductionFromPublishableKey, parsePublishableKey } from '@clerk/shar import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler'; import { isMalformedURLError } from '@clerk/shared/pathMatcher'; import { isAutoProxyDisabledFromEnvironment, shouldAutoProxy } from '@clerk/shared/proxy'; +import { isDevelopmentEnvironment } from '@clerk/shared/utils'; import { notFound as nextjsNotFound } from 'next/navigation'; import type { NextMiddleware, NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; @@ -37,7 +38,7 @@ import { canUseKeyless } from '../utils/feature-flags'; import { clerkClient } from './clerkClient'; import { DOMAIN, PROXY_URL, PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { type ContentSecurityPolicyOptions, createContentSecurityPolicyHeaders } from './content-security-policy'; -import { keylessMissingEnvVars } from './errors'; +import { keylessMissingEnvVars, productionMissingEnvVars } from './errors'; import { errorThrower } from './errorThrower'; import { clerkMiddlewareRequestDataStorage, clerkMiddlewareRequestDataStore } from './middleware-storage'; import { @@ -148,13 +149,19 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl // Handles the case where `options` is a callback function to dynamically access `NextRequest` const resolvedParams = typeof params === 'function' ? await params(request) : params; - const publishableKey = assertKey(resolvedParams.publishableKey || PUBLISHABLE_KEY, () => - errorThrower.throwMissingPublishableKeyError(), - ); + const publishableKey = assertKey(resolvedParams.publishableKey || PUBLISHABLE_KEY, () => { + if (isDevelopmentEnvironment()) { + return errorThrower.throwMissingPublishableKeyError(); + } + throw new Error(productionMissingEnvVars); + }); - const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY, () => - errorThrower.throwMissingSecretKeyError(), - ); + const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY, () => { + if (isDevelopmentEnvironment()) { + return errorThrower.throwMissingSecretKeyError(); + } + throw new Error(productionMissingEnvVars); + }); // Handle Frontend API proxy requests early, before authentication const requestUrl = new URL(request.nextUrl.href); diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 4b2d3e375d2..cdebd4702db 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -42,6 +42,14 @@ To set up Clerk for this project, run: The command is non-interactive and requires no Clerk account or login: it provisions temporary development keys automatically and writes them to your .env.local file. Then restart your dev server. Do not invent placeholder keys - they will fail. If you already have a Clerk application, copy its keys from https://dashboard.clerk.com into .env.local instead. (code=missing_env_keys)`; +export const productionMissingEnvVars = `Clerk: Missing environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY). + +To deploy this application to production, run: + + npx clerk@latest deploy + +This provisions a production Clerk instance and walks you through DNS and OAuth setup. If you already have a production instance, run \`npx clerk@latest env pull --instance prod\` to write its keys to your env file, and set the same keys in your deployment environment. (code=missing_env_keys_production)`; + export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/reference/nextjs/clerk-middleware. (code=auth_signature_invalid)`; export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. For more information, see: https://clerk.com/docs/reference/nextjs/clerk-middleware#dynamic-keys. (code=encryption_key_invalid)`; diff --git a/packages/shared/src/__tests__/error.spec.ts b/packages/shared/src/__tests__/error.spec.ts index 47981be4a4e..6c347f58994 100644 --- a/packages/shared/src/__tests__/error.spec.ts +++ b/packages/shared/src/__tests__/error.spec.ts @@ -22,7 +22,7 @@ describe('ErrorThrower', () => { it('throws the correct error message and interpolates pkg if no parameters are provided', () => { expect(() => errorThrower.throwMissingPublishableKeyError()).toThrow( - '@clerk/test-package: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/test-package: Missing publishableKey. To set up Clerk for this project, run:', ); }); diff --git a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts index 81191d47072..640b73e947d 100644 --- a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts +++ b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts @@ -46,7 +46,7 @@ describe('loadClerkJsScript(options)', () => { test('throws error when publishableKey is missing', async () => { await expect(loadClerkJsScript({} as any)).rejects.toThrow( - '@clerk/react: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/react: Missing publishableKey. To set up Clerk for this project, run:', ); }); @@ -310,7 +310,7 @@ describe('loadClerkUIScript(options)', () => { test('throws error when publishableKey is missing', async () => { await expect(loadClerkUIScript({} as any)).rejects.toThrow( - '@clerk/react: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/react: Missing publishableKey. To set up Clerk for this project, run:', ); }); diff --git a/packages/shared/src/errors/errorThrower.ts b/packages/shared/src/errors/errorThrower.ts index 030e1b68948..1c2c7f5cf42 100644 --- a/packages/shared/src/errors/errorThrower.ts +++ b/packages/shared/src/errors/errorThrower.ts @@ -1,8 +1,16 @@ const DefaultMessages = Object.freeze({ InvalidProxyUrlErrorMessage: `The proxyUrl passed to Clerk is invalid. The expected value for proxyUrl is an absolute URL or a relative path with a leading '/'. (key={{url}})`, InvalidPublishableKeyErrorMessage: `The publishableKey passed to Clerk is invalid. You can get your Publishable key at https://dashboard.clerk.com/last-active?path=api-keys. (key={{key}})`, - MissingPublishableKeyErrorMessage: `Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, - MissingSecretKeyErrorMessage: `Missing secretKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, + MissingPublishableKeyErrorMessage: `Missing publishableKey. To set up Clerk for this project, run: + + npx clerk@latest init + +This creates a Clerk application and writes the required keys to your env file. If you already have a Clerk application, copy the keys from https://dashboard.clerk.com/last-active?path=api-keys instead.`, + MissingSecretKeyErrorMessage: `Missing secretKey. To set up Clerk for this project, run: + + npx clerk@latest init + +This creates a Clerk application and writes the required keys to your env file. If you already have a Clerk application, copy the keys from https://dashboard.clerk.com/last-active?path=api-keys instead.`, MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, });