diff --git a/.changeset/nextjs-cache-components-support.md b/.changeset/nextjs-cache-components-support.md
new file mode 100644
index 00000000000..b8938cffea7
--- /dev/null
+++ b/.changeset/nextjs-cache-components-support.md
@@ -0,0 +1,5 @@
+---
+'@clerk/nextjs': patch
+---
+
+Add support for Next.js 16 cache components by improving error detection and providing helpful error messages when `auth()` or `currentUser()` are called inside a `"use cache"` function.
diff --git a/integration/templates/next-cache-components/src/app/api/use-cache-error-trigger/route.ts b/integration/templates/next-cache-components/src/app/api/use-cache-error-trigger/route.ts
new file mode 100644
index 00000000000..e4e7cb95f03
--- /dev/null
+++ b/integration/templates/next-cache-components/src/app/api/use-cache-error-trigger/route.ts
@@ -0,0 +1,19 @@
+import { auth } from '@clerk/nextjs/server';
+
+// This function deliberately calls auth() inside "use cache" to trigger the error
+async function getCachedAuthData() {
+ 'use cache';
+ // This WILL throw an error because auth() uses headers() internally
+ const { userId } = await auth();
+ return { userId };
+}
+
+export async function GET() {
+ try {
+ const data = await getCachedAuthData();
+ return Response.json(data);
+ } catch (e: any) {
+ // Return the error message so we can verify it in tests
+ return Response.json({ error: e.message }, { status: 500 });
+ }
+}
diff --git a/integration/templates/next-cache-components/src/app/current-user-cache-correct/page.tsx b/integration/templates/next-cache-components/src/app/current-user-cache-correct/page.tsx
new file mode 100644
index 00000000000..a7e4f82f11a
--- /dev/null
+++ b/integration/templates/next-cache-components/src/app/current-user-cache-correct/page.tsx
@@ -0,0 +1,93 @@
+import { Suspense } from 'react';
+import { currentUser, clerkClient } from '@clerk/nextjs/server';
+
+// Simulated cached operation that fetches additional user data
+async function getCachedUserProfile(userId: string) {
+ 'use cache';
+ // This is the CORRECT pattern:
+ // - currentUser() is called OUTSIDE the cache function
+ // - Only the userId is passed into the cache function
+ // - The cache function uses clerkClient() which is allowed in cache contexts
+ const client = await clerkClient();
+ const user = await client.users.getUser(userId);
+
+ return {
+ userId,
+ cachedAt: new Date().toISOString(),
+ profile: {
+ fullName: [user.firstName, user.lastName].filter(Boolean).join(' ') || 'Unknown',
+ emailCount: user.emailAddresses?.length ?? 0,
+ },
+ };
+}
+
+async function CurrentUserCacheContent() {
+ // Step 1: Call currentUser() OUTSIDE the cache function
+ const user = await currentUser();
+
+ if (!user) {
+ return (
+ <>
+
Please sign in to test the caching pattern with currentUser().
+ Not signed in
+ >
+ );
+ }
+
+ // Step 2: Pass userId INTO the cache function
+ const cachedProfile = await getCachedUserProfile(user.id);
+
+ return (
+ <>
+
+ This demonstrates the correct way to use "use cache" with currentUser():
+
+
+ -
+ Call
currentUser() outside the cache function
+
+ -
+ Pass the
userId into the cache function
+
+ -
+ Use
clerkClient() inside the cache function (allowed)
+
+
+
+
+
Cached Profile Data:
+
{JSON.stringify(cachedProfile, null, 2)}
+
+
+ {user.id}
+
+
+ {`
+// Correct pattern:
+const user = await currentUser(); // Outside cache
+if (user) {
+ const profile = await getCachedProfile(user.id); // Pass userId in
+}
+
+async function getCachedProfile(userId: string) {
+ 'use cache';
+ const client = await clerkClient();
+ return client.users.getUser(userId);
+}
+ `}
+
+ >
+ );
+}
+
+export default function CurrentUserCacheCorrectPage() {
+ return (
+
+ currentUser() with "use cache" Correct Pattern
+
+ Loading...}>
+
+
+
+ );
+}
diff --git a/integration/templates/next-cache-components/src/app/current-user-server-component/page.tsx b/integration/templates/next-cache-components/src/app/current-user-server-component/page.tsx
new file mode 100644
index 00000000000..54682ee1d74
--- /dev/null
+++ b/integration/templates/next-cache-components/src/app/current-user-server-component/page.tsx
@@ -0,0 +1,43 @@
+import { Suspense } from 'react';
+import { currentUser } from '@clerk/nextjs/server';
+
+async function CurrentUserContent() {
+ const user = await currentUser();
+
+ return (
+ <>
+
+
Current User Result:
+
+ {JSON.stringify(
+ {
+ id: user?.id ?? null,
+ firstName: user?.firstName ?? null,
+ lastName: user?.lastName ?? null,
+ primaryEmailAddress: user?.primaryEmailAddress?.emailAddress ?? null,
+ isSignedIn: !!user,
+ },
+ null,
+ 2,
+ )}
+
+
+
+ {user?.id ?? 'Not signed in'}
+ {user?.primaryEmailAddress?.emailAddress ?? 'No email'}
+ >
+ );
+}
+
+export default function CurrentUserServerComponentPage() {
+ return (
+
+ currentUser() in Server Component
+ This page tests using currentUser() in a standard React Server Component.
+
+ Loading user...}>
+
+
+
+ );
+}
diff --git a/integration/templates/next-cache-components/src/app/page.tsx b/integration/templates/next-cache-components/src/app/page.tsx
index d76a0666b61..5d863924bba 100644
--- a/integration/templates/next-cache-components/src/app/page.tsx
+++ b/integration/templates/next-cache-components/src/app/page.tsx
@@ -12,6 +12,9 @@ export default function Home() {
auth() in Server Component
+
+ currentUser() in Server Component
+
auth() in Server Action
@@ -19,10 +22,16 @@ export default function Home() {
auth() in API Route
- use cache with auth() (should error)
+ use cache with auth() (documentation)
+
+
+ use cache error trigger (actual error)
+
+
+ "use cache" correct pattern (auth)
- "use cache" correct pattern
+ "use cache" correct pattern (currentUser)
PPR with auth()
diff --git a/integration/templates/next-cache-components/src/app/use-cache-error-trigger/page.tsx b/integration/templates/next-cache-components/src/app/use-cache-error-trigger/page.tsx
new file mode 100644
index 00000000000..2acba59609f
--- /dev/null
+++ b/integration/templates/next-cache-components/src/app/use-cache-error-trigger/page.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+export default function UseCacheErrorTriggerPage() {
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const triggerError = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await fetch('/api/use-cache-error-trigger');
+ const data = await response.json();
+ if (data.error) {
+ setError(data.error);
+ }
+ } catch (e: any) {
+ setError(e.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ triggerError();
+ }, []);
+
+ return (
+
+ "use cache" Error Trigger
+ This page triggers an actual error by calling auth() inside a "use cache" function.
+
+ {loading && Loading...
}
+
+ {error && (
+
+
Error Caught:
+
{error}
+
+ )}
+
+
+
+ );
+}
diff --git a/integration/tests/cache-components.test.ts b/integration/tests/cache-components.test.ts
index 356b030b328..a1013250dbf 100644
--- a/integration/tests/cache-components.test.ts
+++ b/integration/tests/cache-components.test.ts
@@ -27,6 +27,9 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern:
await u.page.goToRelative('/');
await expect(u.page.getByText('Next.js Cache Components Test App')).toBeVisible();
await expect(u.page.getByRole('link', { name: 'auth() in Server Component' })).toBeVisible();
+ await expect(u.page.getByRole('link', { name: 'currentUser() in Server Component' })).toBeVisible();
+ await expect(u.page.getByRole('link', { name: '"use cache" correct pattern (auth)' })).toBeVisible();
+ await expect(u.page.getByRole('link', { name: '"use cache" correct pattern (currentUser)' })).toBeVisible();
});
test('auth() in server component works when signed out', async ({ page, context }) => {
@@ -58,6 +61,41 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern:
expect(userId).toMatch(/^user_/);
});
+ test('currentUser() in server component works when signed out', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/current-user-server-component');
+ await expect(u.page.getByText('currentUser() in Server Component')).toBeVisible();
+ await expect(u.page.getByTestId('current-user-id')).toContainText('Not signed in');
+ });
+
+ test('currentUser() in server component works when signed in', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ // Sign in first
+ await u.po.signIn.goTo();
+ await u.po.signIn.signInWithEmailAndInstantPassword({
+ email: fakeUser.email,
+ password: fakeUser.password,
+ });
+ await u.po.expect.toBeSignedIn();
+
+ // Navigate to server component page
+ await u.page.goToRelative('/current-user-server-component');
+ await expect(u.page.getByText('currentUser() in Server Component')).toBeVisible();
+
+ // Should show user ID (starts with 'user_')
+ const userIdElement = u.page.getByTestId('current-user-id');
+ await expect(userIdElement).toBeVisible();
+ const userId = await userIdElement.textContent();
+ expect(userId).toMatch(/^user_/);
+
+ // Should also show the email
+ const emailElement = u.page.getByTestId('current-user-email');
+ await expect(emailElement).toBeVisible();
+ const email = await emailElement.textContent();
+ expect(email).toContain('@');
+ });
+
test('auth() in server action works', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
@@ -102,7 +140,18 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern:
expect(data.isSignedIn).toBe(true);
});
- test('"use cache" correct pattern works', async ({ page, context }) => {
+ test('"use cache" correct pattern with auth() works when signed out', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ // Navigate to correct pattern page without signing in
+ await u.page.goToRelative('/use-cache-correct');
+ await expect(u.page.getByText('"use cache" Correct Pattern')).toBeVisible();
+
+ // Should show signed out message
+ await expect(u.page.getByTestId('signed-out')).toBeVisible();
+ });
+
+ test('"use cache" correct pattern with auth() works when signed in', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
// Sign in first
@@ -124,6 +173,45 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern:
expect(dataText).toContain('userId');
});
+ test('"use cache" correct pattern with currentUser() works when signed out', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ // Navigate to correct pattern page without signing in
+ await u.page.goToRelative('/current-user-cache-correct');
+ await expect(u.page.getByText('currentUser() with "use cache" Correct Pattern')).toBeVisible();
+
+ // Should show signed out message
+ await expect(u.page.getByTestId('signed-out')).toBeVisible();
+ });
+
+ test('"use cache" correct pattern with currentUser() works when signed in', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ // Sign in first
+ await u.po.signIn.goTo();
+ await u.po.signIn.signInWithEmailAndInstantPassword({
+ email: fakeUser.email,
+ password: fakeUser.password,
+ });
+ await u.po.expect.toBeSignedIn();
+
+ // Navigate to correct pattern page
+ await u.page.goToRelative('/current-user-cache-correct');
+ await expect(u.page.getByText('currentUser() with "use cache" Correct Pattern')).toBeVisible();
+
+ // Should show cached profile with user ID
+ const cachedProfile = u.page.getByTestId('cached-profile');
+ await expect(cachedProfile).toBeVisible();
+ const profileText = await cachedProfile.textContent();
+ expect(profileText).toContain('userId');
+
+ // Should also show the user ID
+ const userIdElement = u.page.getByTestId('current-user-id');
+ await expect(userIdElement).toBeVisible();
+ const userId = await userIdElement.textContent();
+ expect(userId).toMatch(/^user_/);
+ });
+
test('"use cache" error documentation page loads', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/use-cache-error');
@@ -131,7 +219,39 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern:
await expect(u.page.getByTestId('expected-error')).toBeVisible();
});
- test('PPR with auth() renders correctly', async ({ page, context }) => {
+ test('auth() inside "use cache" shows helpful Clerk error message', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ // Navigate to the error trigger page
+ await u.page.goToRelative('/use-cache-error-trigger');
+ await expect(u.page.getByText('"use cache" Error Trigger')).toBeVisible();
+
+ // Wait for the error to be displayed
+ const errorMessage = u.page.getByTestId('error-message');
+ await expect(errorMessage).toBeVisible({ timeout: 10000 });
+
+ // Verify the error contains our custom Clerk error message
+ const errorText = await errorMessage.textContent();
+ expect(errorText).toContain('Clerk:');
+ expect(errorText).toContain('auth() and currentUser() cannot be called inside a "use cache" function');
+ expect(errorText).toContain('headers()');
+ });
+
+ test('PPR with auth() renders correctly when signed out', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ // Navigate to PPR page without signing in
+ await u.page.goToRelative('/ppr-auth');
+ await expect(u.page.getByText('PPR with auth()')).toBeVisible();
+
+ // Static content should be visible (pre-rendered shell)
+ await expect(u.page.getByTestId('static-content')).toBeVisible();
+
+ // Dynamic content should stream in even when signed out
+ await expect(u.page.getByTestId('dynamic-content')).toBeVisible();
+ });
+
+ test('PPR with auth() renders correctly when signed in', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
// Sign in first
diff --git a/packages/nextjs/src/app-router/server/__tests__/utils.test.ts b/packages/nextjs/src/app-router/server/__tests__/utils.test.ts
new file mode 100644
index 00000000000..170dbc590fe
--- /dev/null
+++ b/packages/nextjs/src/app-router/server/__tests__/utils.test.ts
@@ -0,0 +1,132 @@
+import { describe, expect, it } from 'vitest';
+
+import { ClerkUseCacheError, isClerkUseCacheError, isNextjsUseCacheError, isPrerenderingBailout } from '../utils';
+
+describe('isPrerenderingBailout', () => {
+ it('returns false for non-Error values', () => {
+ expect(isPrerenderingBailout(null)).toBe(false);
+ expect(isPrerenderingBailout(undefined)).toBe(false);
+ expect(isPrerenderingBailout('string')).toBe(false);
+ expect(isPrerenderingBailout(123)).toBe(false);
+ expect(isPrerenderingBailout({})).toBe(false);
+ });
+
+ it('returns true for dynamic server usage errors', () => {
+ const error = new Error('Dynamic server usage: headers');
+ expect(isPrerenderingBailout(error)).toBe(true);
+ });
+
+ it('returns true for bail out of prerendering errors', () => {
+ const error = new Error('This page needs to bail out of prerendering');
+ expect(isPrerenderingBailout(error)).toBe(true);
+ });
+
+ it('returns true for route prerendering bailout errors (Next.js 14.1.1+)', () => {
+ const error = new Error(
+ 'Route /example needs to bail out of prerendering at this point because it used headers().',
+ );
+ expect(isPrerenderingBailout(error)).toBe(true);
+ });
+
+ it('returns true for headers() rejection during prerendering (Next.js 16 cacheComponents)', () => {
+ const error = new Error(
+ 'During prerendering, `headers()` rejects when the prerender is complete. ' +
+ 'Typically these errors are handled by React but if you move `headers()` to a different context ' +
+ 'by using `setTimeout`, `after`, or similar functions you may observe this error and you should handle it in that context.',
+ );
+ expect(isPrerenderingBailout(error)).toBe(true);
+ });
+
+ it('returns false for unrelated errors', () => {
+ const error = new Error('Some other error');
+ expect(isPrerenderingBailout(error)).toBe(false);
+ });
+});
+
+describe('ClerkUseCacheError', () => {
+ it('is recognized by isClerkUseCacheError', () => {
+ const error = new ClerkUseCacheError('Test message');
+ expect(isClerkUseCacheError(error)).toBe(true);
+ });
+
+ it('preserves original error', () => {
+ const original = new Error('Original');
+ const error = new ClerkUseCacheError('Wrapped', original);
+ expect(error.originalError).toBe(original);
+ });
+
+ it('has correct name', () => {
+ const error = new ClerkUseCacheError('Test');
+ expect(error.name).toBe('ClerkUseCacheError');
+ });
+});
+
+describe('isClerkUseCacheError', () => {
+ it('returns false for regular errors', () => {
+ expect(isClerkUseCacheError(new Error('test'))).toBe(false);
+ });
+
+ it('returns false for non-Error values', () => {
+ expect(isClerkUseCacheError(null)).toBe(false);
+ expect(isClerkUseCacheError('string')).toBe(false);
+ expect(isClerkUseCacheError({})).toBe(false);
+ });
+
+ it('returns true for ClerkUseCacheError', () => {
+ expect(isClerkUseCacheError(new ClerkUseCacheError('test'))).toBe(true);
+ });
+});
+
+describe('isNextjsUseCacheError', () => {
+ it('returns false for non-Error values', () => {
+ expect(isNextjsUseCacheError(null)).toBe(false);
+ expect(isNextjsUseCacheError(undefined)).toBe(false);
+ expect(isNextjsUseCacheError('string')).toBe(false);
+ expect(isNextjsUseCacheError(123)).toBe(false);
+ expect(isNextjsUseCacheError({})).toBe(false);
+ });
+
+ it('returns true for "use cache" errors with double quotes', () => {
+ const error = new Error('Route /example used `headers()` inside "use cache"');
+ expect(isNextjsUseCacheError(error)).toBe(true);
+ });
+
+ it("returns true for 'use cache' errors with single quotes", () => {
+ const error = new Error("Route /example used `cookies()` inside 'use cache'");
+ expect(isNextjsUseCacheError(error)).toBe(true);
+ });
+
+ it('returns true for cache scope errors', () => {
+ const error = new Error(
+ 'Accessing Dynamic data sources inside a cache scope is not supported. ' +
+ 'If you need this data inside a cached function use `headers()` outside of the cached function.',
+ );
+ expect(isNextjsUseCacheError(error)).toBe(true);
+ });
+
+ it('returns false for generic "cache" mentions without specific patterns', () => {
+ // This should NOT match to reduce false positives - requires "cache scope" not just "cache"
+ const error = new Error('Dynamic data source accessed in cache context');
+ expect(isNextjsUseCacheError(error)).toBe(false);
+ });
+
+ it('returns false for regular prerendering bailout errors', () => {
+ const error = new Error('Dynamic server usage: headers');
+ expect(isNextjsUseCacheError(error)).toBe(false);
+ });
+
+ it('returns false for unrelated errors', () => {
+ const error = new Error('Some other error');
+ expect(isNextjsUseCacheError(error)).toBe(false);
+ });
+
+ it('returns true for the exact Next.js 16 error message', () => {
+ const error = new Error(
+ 'Route /examples/cached-components used `headers()` inside "use cache". ' +
+ 'Accessing Dynamic data sources inside a cache scope is not supported. ' +
+ 'If you need this data inside a cached function use `headers()` outside of the cached function ' +
+ 'and pass the required dynamic data in as an argument.',
+ );
+ expect(isNextjsUseCacheError(error)).toBe(true);
+ });
+});
diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts
index af7af1d44a4..f1bae2c325e 100644
--- a/packages/nextjs/src/app-router/server/auth.ts
+++ b/packages/nextjs/src/app-router/server/auth.ts
@@ -12,7 +12,13 @@ import type { AuthProtect } from '../../server/protect';
import { createProtect } from '../../server/protect';
import { decryptClerkRequestData } from '../../server/utils';
import { middlewareFileReference } from '../../utils/sdk-versions';
-import { buildRequestLike } from './utils';
+import {
+ buildRequestLike,
+ ClerkUseCacheError,
+ isClerkUseCacheError,
+ isNextjsUseCacheError,
+ USE_CACHE_ERROR_MESSAGE,
+} from './utils';
/**
* `Auth` object of the currently active user and the `redirectToSignIn()` method.
@@ -72,73 +78,83 @@ export const auth: AuthFn = (async (options?: AuthOptions) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('server-only');
- const request = await buildRequestLike();
-
- const stepsBasedOnSrcDirectory = async () => {
- try {
- const isSrcAppDir = await import('../../server/fs/middleware-location.js').then(m => m.hasSrcAppDir());
- const fileName =
- middlewareFileReference === 'middleware or proxy'
- ? 'middleware.(ts|js) or proxy.(ts|js)'
- : 'middleware.(ts|js)';
- return [`Your ${middlewareFileReference} file exists at ./${isSrcAppDir ? 'src/' : ''}${fileName}`];
- } catch {
- return [];
- }
- };
- const authObject = await createAsyncGetAuth({
- debugLoggerName: 'auth()',
- noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory(), middlewareFileReference),
- })(request, {
- treatPendingAsSignedOut: options?.treatPendingAsSignedOut,
- acceptsToken: options?.acceptsToken ?? TokenType.SessionToken,
- });
-
- const clerkUrl = getAuthKeyFromRequest(request, 'ClerkUrl');
-
- const createRedirectForRequest = (...args: Parameters>) => {
- const { returnBackUrl } = args[0] || {};
- const clerkRequest = createClerkRequest(request);
- const devBrowserToken =
- clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) ||
- clerkRequest.cookies.get(constants.Cookies.DevBrowser);
-
- const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData);
- const decryptedRequestData = decryptClerkRequestData(encryptedRequestData);
- return [
- createRedirect({
- redirectAdapter: redirect,
- devBrowserToken: devBrowserToken,
- baseUrl: clerkRequest.clerkUrl.toString(),
- publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY,
- signInUrl: decryptedRequestData.signInUrl || SIGN_IN_URL,
- signUpUrl: decryptedRequestData.signUpUrl || SIGN_UP_URL,
- sessionStatus: authObject.tokenType === TokenType.SessionToken ? authObject.sessionStatus : null,
- isSatellite: decryptedRequestData.isSatellite,
- }),
- returnBackUrl === null ? '' : returnBackUrl || clerkUrl?.toString(),
- ] as const;
- };
-
- const redirectToSignIn: RedirectFun = (opts = {}) => {
- const [r, returnBackUrl] = createRedirectForRequest(opts);
- return r.redirectToSignIn({
- returnBackUrl,
+ try {
+ const request = await buildRequestLike();
+
+ const stepsBasedOnSrcDirectory = async () => {
+ try {
+ const isSrcAppDir = await import('../../server/fs/middleware-location.js').then(m => m.hasSrcAppDir());
+ const fileName =
+ middlewareFileReference === 'middleware or proxy'
+ ? 'middleware.(ts|js) or proxy.(ts|js)'
+ : 'middleware.(ts|js)';
+ return [`Your ${middlewareFileReference} file exists at ./${isSrcAppDir ? 'src/' : ''}${fileName}`];
+ } catch {
+ return [];
+ }
+ };
+ const authObject = await createAsyncGetAuth({
+ debugLoggerName: 'auth()',
+ noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory(), middlewareFileReference),
+ })(request, {
+ treatPendingAsSignedOut: options?.treatPendingAsSignedOut,
+ acceptsToken: options?.acceptsToken ?? TokenType.SessionToken,
});
- };
- const redirectToSignUp: RedirectFun = (opts = {}) => {
- const [r, returnBackUrl] = createRedirectForRequest(opts);
- return r.redirectToSignUp({
- returnBackUrl,
- });
- };
+ const clerkUrl = getAuthKeyFromRequest(request, 'ClerkUrl');
+
+ const createRedirectForRequest = (...args: Parameters>) => {
+ const { returnBackUrl } = args[0] || {};
+ const clerkRequest = createClerkRequest(request);
+ const devBrowserToken =
+ clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) ||
+ clerkRequest.cookies.get(constants.Cookies.DevBrowser);
+
+ const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData);
+ const decryptedRequestData = decryptClerkRequestData(encryptedRequestData);
+ return [
+ createRedirect({
+ redirectAdapter: redirect,
+ devBrowserToken: devBrowserToken,
+ baseUrl: clerkRequest.clerkUrl.toString(),
+ publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY,
+ signInUrl: decryptedRequestData.signInUrl || SIGN_IN_URL,
+ signUpUrl: decryptedRequestData.signUpUrl || SIGN_UP_URL,
+ sessionStatus: authObject.tokenType === TokenType.SessionToken ? authObject.sessionStatus : null,
+ isSatellite: decryptedRequestData.isSatellite,
+ }),
+ returnBackUrl === null ? '' : returnBackUrl || clerkUrl?.toString(),
+ ] as const;
+ };
+
+ const redirectToSignIn: RedirectFun = (opts = {}) => {
+ const [r, returnBackUrl] = createRedirectForRequest(opts);
+ return r.redirectToSignIn({
+ returnBackUrl,
+ });
+ };
+
+ const redirectToSignUp: RedirectFun = (opts = {}) => {
+ const [r, returnBackUrl] = createRedirectForRequest(opts);
+ return r.redirectToSignUp({
+ returnBackUrl,
+ });
+ };
+
+ if (authObject.tokenType === TokenType.SessionToken) {
+ return Object.assign(authObject, { redirectToSignIn, redirectToSignUp });
+ }
- if (authObject.tokenType === TokenType.SessionToken) {
- return Object.assign(authObject, { redirectToSignIn, redirectToSignUp });
+ return authObject;
+ } catch (e: any) {
+ if (isClerkUseCacheError(e)) {
+ throw e;
+ }
+ if (isNextjsUseCacheError(e)) {
+ throw new ClerkUseCacheError(`${USE_CACHE_ERROR_MESSAGE}\n\nOriginal error: ${e.message}`, e);
+ }
+ throw e;
}
-
- return authObject;
}) as AuthFn;
auth.protect = async (...args: any[]) => {
diff --git a/packages/nextjs/src/app-router/server/currentUser.ts b/packages/nextjs/src/app-router/server/currentUser.ts
index 99c90113d04..02ac8fabfb6 100644
--- a/packages/nextjs/src/app-router/server/currentUser.ts
+++ b/packages/nextjs/src/app-router/server/currentUser.ts
@@ -3,6 +3,7 @@ import type { PendingSessionOptions } from '@clerk/shared/types';
import { clerkClient } from '../../server/clerkClient';
import { auth } from './auth';
+import { ClerkUseCacheError, isClerkUseCacheError, isNextjsUseCacheError, USE_CACHE_ERROR_MESSAGE } from './utils';
type CurrentUserOptions = PendingSessionOptions;
@@ -31,10 +32,20 @@ export async function currentUser(opts?: CurrentUserOptions): Promise {
+ return e instanceof Error && CLERK_USE_CACHE_MARKER in e;
+};
+
+// Patterns for Next.js "use cache" errors - tightened to reduce false positives
+// Matches both "use cache" (double quotes) and 'use cache' (single quotes)
+const USE_CACHE_WITH_DYNAMIC_API_PATTERN =
+ /inside\s+["']use cache["']|["']use cache["'].*(?:headers|cookies)|(?:headers|cookies).*["']use cache["']/i;
+const CACHE_SCOPE_PATTERN = /cache scope/i;
+const DYNAMIC_DATA_SOURCE_PATTERN = /dynamic data source/i;
+// https://github.com/vercel/next.js/pull/61332
+const ROUTE_BAILOUT_PATTERN = /Route .*? needs to bail out of prerendering at this point because it used .*?./;
+
export const isPrerenderingBailout = (e: unknown) => {
if (!(e instanceof Error) || !('message' in e)) {
return false;
}
const { message } = e;
-
const lowerCaseInput = message.toLowerCase();
- const dynamicServerUsage = lowerCaseInput.includes('dynamic server usage');
- const bailOutPrerendering = lowerCaseInput.includes('this page needs to bail out of prerendering');
- // note: new error message syntax introduced in next@14.1.1-canary.21
- // but we still want to support older versions.
- // https://github.com/vercel/next.js/pull/61332 (dynamic-rendering.ts:153)
- const routeRegex = /Route .*? needs to bail out of prerendering at this point because it used .*?./;
+ return (
+ ROUTE_BAILOUT_PATTERN.test(message) ||
+ lowerCaseInput.includes('dynamic server usage') ||
+ lowerCaseInput.includes('this page needs to bail out of prerendering') ||
+ lowerCaseInput.includes('during prerendering')
+ );
+};
+
+/**
+ * Detects Next.js errors from using dynamic APIs (headers/cookies) inside "use cache".
+ */
+export const isNextjsUseCacheError = (e: unknown): boolean => {
+ if (!(e instanceof Error)) {
+ return false;
+ }
+
+ const { message } = e;
+
+ // "use cache" with dynamic API context (e.g., 'used `headers()` inside "use cache"')
+ if (USE_CACHE_WITH_DYNAMIC_API_PATTERN.test(message)) {
+ return true;
+ }
- return routeRegex.test(message) || dynamicServerUsage || bailOutPrerendering;
+ // "cache scope" with dynamic data source (e.g., 'Dynamic data sources inside a cache scope')
+ if (CACHE_SCOPE_PATTERN.test(message) && DYNAMIC_DATA_SOURCE_PATTERN.test(message)) {
+ return true;
+ }
+
+ return false;
};
+export const USE_CACHE_ERROR_MESSAGE =
+ `Clerk: auth() and currentUser() cannot be called inside a "use cache" function. ` +
+ `These functions access \`headers()\` internally, which is a dynamic API not allowed in cached contexts.\n\n` +
+ `To fix this, call auth() outside the cached function and pass the values you need as arguments:\n\n` +
+ ` import { auth, clerkClient } from '@clerk/nextjs/server';\n\n` +
+ ` async function getCachedUser(userId: string) {\n` +
+ ` "use cache";\n` +
+ ` const client = await clerkClient();\n` +
+ ` return client.users.getUser(userId);\n` +
+ ` }\n\n` +
+ ` // In your component/page:\n` +
+ ` const { userId } = await auth();\n` +
+ ` if (userId) {\n` +
+ ` const user = await getCachedUser(userId);\n` +
+ ` }`;
+
export async function buildRequestLike(): Promise {
try {
- // Dynamically import next/headers, otherwise Next12 apps will break
- // @ts-expect-error: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307)
+ // @ts-expect-error - Dynamically import to avoid breaking Next 12 apps
const { headers } = await import('next/headers');
const resolvedHeaders = await headers();
return new NextRequest('https://placeholder.com', { headers: resolvedHeaders });
} catch (e: any) {
- // rethrow the error when react throws a prerendering bailout
// https://nextjs.org/docs/messages/ppr-caught-error
if (e && isPrerenderingBailout(e)) {
throw e;
}
+ if (e && isNextjsUseCacheError(e)) {
+ throw new ClerkUseCacheError(`${USE_CACHE_ERROR_MESSAGE}\n\nOriginal error: ${e.message}`, e);
+ }
+
throw new Error(
`Clerk: auth(), currentUser() and clerkClient(), are only supported in App Router (/app directory).\nIf you're using /pages, try getAuth() instead.\nOriginal error: ${e}`,
);
diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts
index 907d7db1e66..eb70784b69d 100644
--- a/packages/nextjs/src/server/clerkClient.ts
+++ b/packages/nextjs/src/server/clerkClient.ts
@@ -1,6 +1,6 @@
import { constants } from '@clerk/backend/internal';
-import { buildRequestLike, isPrerenderingBailout } from '../app-router/server/utils';
+import { buildRequestLike, isClerkUseCacheError, isPrerenderingBailout } from '../app-router/server/utils';
import { createClerkClientWithOptions } from './createClerkClient';
import { getHeader } from './headers-utils';
import { clerkMiddlewareRequestDataStorage } from './middleware-storage';
@@ -21,6 +21,9 @@ const clerkClient = async () => {
if (err && isPrerenderingBailout(err)) {
throw err;
}
+ if (err && isClerkUseCacheError(err)) {
+ throw err;
+ }
}
// Fallbacks between options from middleware runtime and `NextRequest` from application server