Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-streets-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/nextjs': patch
---

Fix prerendering errors when using `auth()` with Next.js 16 Cache Components.
83 changes: 83 additions & 0 deletions packages/nextjs/src/app-router/server/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest';

import { 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 false for unrelated errors', () => {
const error = new Error('Some other error');
expect(isPrerenderingBailout(error)).toBe(false);
});
});

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', () => {
const error = new Error('Route /example used `headers()` 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 true for dynamic data source cache errors', () => {
const error = new Error('Dynamic data source accessed in cache context');
expect(isNextjsUseCacheError(error)).toBe(true);
});

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);
});
});
17 changes: 17 additions & 0 deletions packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ export const auth: AuthFn = (async (options?: AuthOptions) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('server-only');

// Call connection() first to explicitly opt out of prerendering when using Next.js 16 Cache Components
// This prevents "During prerendering, headers() rejects" errors during build
try {
// @ts-expect-error: connection() only exists in Next.js 16+
const { connection } = await import('next/server');
if (typeof connection === 'function') {
await connection();
}
} catch (error) {
// If this is a prerendering bailout, re-throw it
const { isPrerenderingBailout } = await import('./utils.js');
if (isPrerenderingBailout(error)) {
throw error;
}
// Otherwise connection() doesn't exist in older Next.js versions, that's fine
}

const request = await buildRequestLike();

const stepsBasedOnSrcDirectory = async () => {
Expand Down
46 changes: 46 additions & 0 deletions packages/nextjs/src/app-router/server/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { NextRequest } from 'next/server';

// Pre-compiled regex patterns for use cache error detection
const USE_CACHE_SIMPLE_PATTERN = /use cache|cache scope/i;
const DYNAMIC_CACHE_PATTERN = /dynamic data source/i;

export const isPrerenderingBailout = (e: unknown) => {
if (!(e instanceof Error) || !('message' in e)) {
return false;
Expand All @@ -19,6 +23,27 @@ export const isPrerenderingBailout = (e: unknown) => {
return routeRegex.test(message) || dynamicServerUsage || bailOutPrerendering;
};

/**
* Detects if the error is from using dynamic APIs inside a "use cache" component.
* Next.js 15+ throws specific errors when headers(), cookies(), or other dynamic
* APIs are accessed inside a cache scope.
*/
export const isNextjsUseCacheError = (e: unknown): boolean => {
if (!(e instanceof Error)) {
return false;
}

const { message } = e;

// Short-circuit: check simple patterns first
if (USE_CACHE_SIMPLE_PATTERN.test(message)) {
return true;
}

// Check compound pattern: requires both "dynamic data source" AND "cache"
return DYNAMIC_CACHE_PATTERN.test(message) && message.toLowerCase().includes('cache');
};

export async function buildRequestLike(): Promise<NextRequest> {
try {
// Dynamically import next/headers, otherwise Next12 apps will break
Expand All @@ -33,6 +58,27 @@ export async function buildRequestLike(): Promise<NextRequest> {
throw e;
}

// Provide a more helpful error message for "use cache" components
if (e && isNextjsUseCacheError(e)) {
throw new Error(
`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 userId as an argument:\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` +
` }\n\n` +
`Original error: ${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}`,
);
Expand Down
6 changes: 5 additions & 1 deletion packages/nextjs/src/server/clerkClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { constants } from '@clerk/backend/internal';

import { buildRequestLike, isPrerenderingBailout } from '../app-router/server/utils';
import { buildRequestLike, isNextjsUseCacheError, isPrerenderingBailout } from '../app-router/server/utils';
import { createClerkClientWithOptions } from './createClerkClient';
import { getHeader } from './headers-utils';
import { clerkMiddlewareRequestDataStorage } from './middleware-storage';
Expand All @@ -21,6 +21,10 @@ const clerkClient = async () => {
if (err && isPrerenderingBailout(err)) {
throw err;
}
// Re-throw "use cache" errors with the helpful message from buildRequestLike
if (err && isNextjsUseCacheError(err)) {
throw err;
}
}

// Fallbacks between options from middleware runtime and `NextRequest` from application server
Expand Down
Loading