From 0195511d78275a465d8fd4eeb432465993bf7ea7 Mon Sep 17 00:00:00 2001 From: Devin Gould Date: Tue, 18 Aug 2026 11:13:04 -0700 Subject: [PATCH] feat(nextjs): point production and invalid-key errors at the Clerk CLI Co-Authored-By: Claude Fable 5 --- .changeset/nextjs-cli-key-errors.md | 5 ++ .../server/__tests__/clerkMiddleware.test.ts | 6 +- .../clerkMiddlewareInvalidKeys.test.ts | 59 +++++++++++++++++++ packages/nextjs/src/server/clerkMiddleware.ts | 22 +++++-- packages/nextjs/src/server/errors.ts | 24 ++++++++ 5 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 .changeset/nextjs-cli-key-errors.md create mode 100644 packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts diff --git a/.changeset/nextjs-cli-key-errors.md b/.changeset/nextjs-cli-key-errors.md new file mode 100644 index 00000000000..105c3d5c3b8 --- /dev/null +++ b/.changeset/nextjs-cli-key-errors.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +`clerkMiddleware()` now points key misconfiguration at the Clerk CLI: missing keys outside development throw a deploy-oriented error recommending `npx clerk@latest deploy` (`code=missing_env_keys_production`), and the publishable key format is validated upfront, throwing `npx clerk@latest init` guidance in development or `npx clerk@latest env pull --instance prod` guidance in production (`code=invalid_env_keys`, `code=invalid_env_keys_production`) instead of failing later with `Publishable key not valid.` diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 31757419d0f..2d6085ee140 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -266,7 +266,7 @@ describe('clerkMiddleware(params)', () => { it('propagates middleware dynamic keys to the next request', async () => { const options = { secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', - publishableKey: 'pk_test_xxxxxxxxxxxxx', + publishableKey: 'pk_test_ZHluYW1pYy1rZXlzLmNsZXJrLmFjY291bnRzLmRldiQ', signInUrl: '/foo', signUpUrl: '/bar', }; @@ -286,7 +286,7 @@ describe('clerkMiddleware(params)', () => { it('with synchronous callback', async () => { const options = { secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', - publishableKey: 'pk_test_xxxxxxxxxxxxx', + publishableKey: 'pk_test_ZHluYW1pYy1rZXlzLmNsZXJrLmFjY291bnRzLmRldiQ', signInUrl: '/foo', signUpUrl: '/bar', }; @@ -313,7 +313,7 @@ describe('clerkMiddleware(params)', () => { it('with asynchronous callback', async () => { const options = { secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', - publishableKey: 'pk_test_xxxxxxxxxxxxx', + publishableKey: 'pk_test_ZHluYW1pYy1rZXlzLmNsZXJrLmFjY291bnRzLmRldiQ', signInUrl: '/foo', signUpUrl: '/bar', }; diff --git a/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts new file mode 100644 index 00000000000..993c2ed8c76 --- /dev/null +++ b/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts @@ -0,0 +1,59 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; +import type { NextFetchEvent } from 'next/server'; +import { NextRequest } from 'next/server'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// The mock SHOULD exist before the imports: keys are present but not parseable as Clerk keys, so +// the invalid-key error path is reachable. +vi.mock(import('../constants.js'), async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + PUBLISHABLE_KEY: 'pk_test_placeholder', + SECRET_KEY: 'sk_test_placeholder', + }; +}); + +describe('clerkMiddleware when Clerk env vars are invalid', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + vi.resetModules(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + const runMiddleware = async () => { + const { clerkMiddleware } = await import('../clerkMiddleware.js'); + const request = new NextRequest('https://example.com/protected'); + return clerkMiddleware()(request, {} as NextFetchEvent); + }; + + it('throws the invalid-key error pointing at the CLI', async () => { + await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest init/); + await expect(runMiddleware()).rejects.toThrow(/\(code=invalid_env_keys\)/); + }); + + it('throws the production invalid-key error pointing at env pull', async () => { + vi.stubEnv('NODE_ENV', 'production'); + await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest env pull --instance prod/); + await expect(runMiddleware()).rejects.toThrow(/\(code=invalid_env_keys_production\)/); + }); + + it('names the env var and the expected key format in the message', async () => { + const { invalidEnvKeys, productionInvalidEnvKeys } = await import('../errors.js'); + for (const message of [invalidEnvKeys, productionInvalidEnvKeys]) { + expect(message).toContain('NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'); + expect(message).toContain('pk_test_'); + } + expect(invalidEnvKeys).toContain('npx clerk@latest init'); + expect(productionInvalidEnvKeys).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 9e6699de60d..d20ee58d05a 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -24,6 +24,7 @@ import { import { clerkFrontendApiProxy, DEFAULT_PROXY_PATH, matchProxyPath } from '@clerk/backend/proxy'; import { isProductionFromPublishableKey, parsePublishableKey } from '@clerk/shared/keys'; import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler'; +import { isDevelopmentEnvironment } from '@clerk/shared/utils'; import { isMalformedURLError } from '@clerk/shared/pathMatcher'; import { isAutoProxyDisabledFromEnvironment, shouldAutoProxy } from '@clerk/shared/proxy'; import { notFound as nextjsNotFound } from 'next/navigation'; @@ -41,6 +42,7 @@ import { DOMAIN, PROXY_URL, PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_UR import { type ContentSecurityPolicyOptions, createContentSecurityPolicyHeaders } from './content-security-policy'; import { errorThrower } from './errorThrower'; import { getHeader } from './headers-utils'; +import { invalidEnvKeys, productionInvalidEnvKeys, productionMissingEnvVars } from './errors'; import { getKeylessCookieValue } from './keyless'; import { clerkMiddlewareRequestDataStorage, clerkMiddlewareRequestDataStore } from './middleware-storage'; import { @@ -155,12 +157,24 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl const publishableKey = assertKey( resolvedParams.publishableKey || PUBLISHABLE_KEY || keyless?.publishableKey, - () => errorThrower.throwMissingPublishableKeyError(), + () => { + if (isDevelopmentEnvironment()) { + return errorThrower.throwMissingPublishableKeyError(); + } + throw new Error(productionMissingEnvVars); + }, ); - const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey, () => - errorThrower.throwMissingSecretKeyError(), - ); + const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey, () => { + if (isDevelopmentEnvironment()) { + return errorThrower.throwMissingSecretKeyError(); + } + throw new Error(productionMissingEnvVars); + }); + + if (!parsePublishableKey(publishableKey)) { + throw new Error(isDevelopmentEnvironment() ? invalidEnvKeys : productionInvalidEnvKeys); + } // 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 2cc8a8f1c7a..a8f93974ebe 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -34,6 +34,30 @@ For more details, see https://clerk.com/err/auth-middleware `; }; +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 invalidEnvKeys = `Clerk: Invalid environment keys. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is set but is not a valid Clerk publishable key (expected format: pk_test_... or pk_live_...). + +To set up Clerk for this project with valid keys, run: + + npx clerk@latest init + +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=invalid_env_keys)`; + +export const productionInvalidEnvKeys = `Clerk: Invalid environment keys. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is set but is not a valid Clerk publishable key (expected format: pk_test_... or pk_live_...). + +To write the keys of an existing Clerk instance to your env file, run: + + npx clerk@latest env pull --instance prod + +Set the same keys in your deployment environment. If you do not have a production Clerk instance yet, run \`npx clerk@latest deploy\` to provision one. (code=invalid_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)`;