Skip to content

Commit 3e932b4

Browse files
authored
feat(nextjs): Hint correct middleware location when missing clerkMiddleware (#4979)
1 parent 93ae27c commit 3e932b4

File tree

9 files changed

+126
-24
lines changed

9 files changed

+126
-24
lines changed

.changeset/fuzzy-rockets-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/nextjs': patch
3+
---
4+
5+
Improve error messages when `clerkMiddleware` is missing by suggesting the correct path to place the `middleware.ts` file.

packages/nextjs/src/app-router/server/ClerkProvider.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getDynamicAuthData } from '../../server/buildClerkProps';
77
import type { NextClerkProviderProps } from '../../types';
88
import { canUseKeyless } from '../../utils/feature-flags';
99
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
10+
import { onlyTry } from '../../utils/only-try';
1011
import { isNext13 } from '../../utils/sdk-versions';
1112
import { ClientClerkProvider } from '../client/ClerkProvider';
1213
import { deleteKeylessAction } from '../keyless-actions';
@@ -23,15 +24,6 @@ const getNonceFromCSPHeader = React.cache(async function getNonceFromCSPHeader()
2324
return getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || '';
2425
});
2526

26-
/** Discards errors thrown by attempted code */
27-
const onlyTry = (cb: () => unknown) => {
28-
try {
29-
cb();
30-
} catch {
31-
// ignore
32-
}
33-
};
34-
3527
export async function ClerkProvider(
3628
props: Without<NextClerkProviderProps, '__unstable_invokeMiddlewareOnAuthStateChange'>,
3729
) {

packages/nextjs/src/app-router/server/auth.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { constants, createClerkRequest, createRedirect, type RedirectFun } from
33
import { notFound, redirect } from 'next/navigation';
44

55
import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from '../../server/constants';
6-
import { createGetAuth } from '../../server/createGetAuth';
6+
import { createAsyncGetAuth } from '../../server/createGetAuth';
77
import { authAuthHeaderMissing } from '../../server/errors';
88
import { getAuthKeyFromRequest, getHeader } from '../../server/headers-utils';
99
import type { AuthProtect } from '../../server/protect';
1010
import { createProtect } from '../../server/protect';
1111
import { decryptClerkRequestData } from '../../server/utils';
12+
import { isNextWithUnstableServerActions } from '../../utils/sdk-versions';
1213
import { buildRequestLike } from './utils';
1314

1415
/**
@@ -25,8 +26,10 @@ type Auth = AuthObject & {
2526
*/
2627
redirectToSignIn: RedirectFun<ReturnType<typeof redirect>>;
2728
};
29+
2830
export interface AuthFn {
2931
(): Promise<Auth>;
32+
3033
/**
3134
* `auth` includes a single property, the `protect()` method, which you can use in two ways:
3235
* - to check if a user is authenticated (signed in)
@@ -60,9 +63,22 @@ export const auth: AuthFn = async () => {
6063
require('server-only');
6164

6265
const request = await buildRequestLike();
63-
const authObject = createGetAuth({
66+
67+
const stepsBasedOnSrcDirectory = async () => {
68+
if (isNextWithUnstableServerActions) {
69+
return [];
70+
}
71+
72+
try {
73+
const isSrcAppDir = await import('../../server/keyless-node.js').then(m => m.hasSrcAppDir());
74+
return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}middleware.ts`];
75+
} catch {
76+
return [];
77+
}
78+
};
79+
const authObject = await createAsyncGetAuth({
6480
debugLoggerName: 'auth()',
65-
noAuthStatusMessage: authAuthHeaderMissing(),
81+
noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory()),
6682
})(request);
6783

6884
const clerkUrl = getAuthKeyFromRequest(request, 'ClerkUrl');

packages/nextjs/src/app-router/server/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export async function buildRequestLike(): Promise<NextRequest> {
3434
}
3535

3636
throw new Error(
37-
`Clerk: auth() and currentUser() are only supported in App Router (/app directory).\nIf you're using /pages, try getAuth() instead.\nOriginal error: ${e}`,
37+
`Clerk: auth(), currentUser() and clerkClient(), are only supported in App Router (/app directory).\nIf you're using /pages, try getAuth() instead.\nOriginal error: ${e}`,
3838
);
3939
}
4040
}

packages/nextjs/src/server/__tests__/createGetAuth.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import hmacSHA1 from 'crypto-js/hmac-sha1';
33
import { NextRequest } from 'next/server';
44
import { describe, expect, it } from 'vitest';
55

6-
import { createGetAuth, getAuth } from '../createGetAuth';
6+
import { createSyncGetAuth, getAuth } from '../createGetAuth';
77

88
const mockSecretKey = 'sk_test_mock';
99

@@ -16,7 +16,7 @@ const mockTokenSignature = hmacSHA1(mockToken, 'sk_test_mock').toString();
1616

1717
describe('createGetAuth(opts)', () => {
1818
it('returns a getAuth function', () => {
19-
expect(createGetAuth({ debugLoggerName: 'test', noAuthStatusMessage: 'test' })).toBeInstanceOf(Function);
19+
expect(createSyncGetAuth({ debugLoggerName: 'test', noAuthStatusMessage: 'test' })).toBeInstanceOf(Function);
2020
});
2121
});
2222

packages/nextjs/src/server/createGetAuth.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,63 @@
1-
import type { AuthObject } from '@clerk/backend';
21
import { constants } from '@clerk/backend/internal';
32
import { isTruthy } from '@clerk/shared/underscore';
43

54
import { withLogger } from '../utils/debugLogger';
5+
import { isNextWithUnstableServerActions } from '../utils/sdk-versions';
66
import { getAuthDataFromRequest } from './data/getAuthDataFromRequest';
77
import { getAuthAuthHeaderMissing } from './errors';
8-
import { getHeader } from './headers-utils';
8+
import { detectClerkMiddleware, getHeader } from './headers-utils';
99
import type { RequestLike } from './types';
1010
import { assertAuthStatus } from './utils';
1111

12-
export const createGetAuth = ({
12+
export const createAsyncGetAuth = ({
13+
debugLoggerName,
1314
noAuthStatusMessage,
15+
}: {
16+
debugLoggerName: string;
17+
noAuthStatusMessage: string;
18+
}) =>
19+
withLogger(debugLoggerName, logger => {
20+
return async (req: RequestLike, opts?: { secretKey?: string }) => {
21+
if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) {
22+
logger.enable();
23+
}
24+
25+
if (!detectClerkMiddleware(req)) {
26+
// Keep the same behaviour for versions that may have issues with bundling `node:fs`
27+
if (isNextWithUnstableServerActions) {
28+
assertAuthStatus(req, noAuthStatusMessage);
29+
}
30+
31+
const missConfiguredMiddlewareLocation = await import('./keyless-node.js')
32+
.then(m => m.suggestMiddlewareLocation())
33+
.catch(() => undefined);
34+
35+
if (missConfiguredMiddlewareLocation) {
36+
throw new Error(missConfiguredMiddlewareLocation);
37+
}
38+
39+
// still throw there is no suggested move location
40+
assertAuthStatus(req, noAuthStatusMessage);
41+
}
42+
43+
return getAuthDataFromRequest(req, { ...opts, logger });
44+
};
45+
});
46+
47+
export const createSyncGetAuth = ({
1448
debugLoggerName,
49+
noAuthStatusMessage,
1550
}: {
1651
debugLoggerName: string;
1752
noAuthStatusMessage: string;
1853
}) =>
1954
withLogger(debugLoggerName, logger => {
20-
return (req: RequestLike, opts?: { secretKey?: string }): AuthObject => {
55+
return (req: RequestLike, opts?: { secretKey?: string }) => {
2156
if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) {
2257
logger.enable();
2358
}
2459

2560
assertAuthStatus(req, noAuthStatusMessage);
26-
2761
return getAuthDataFromRequest(req, { ...opts, logger });
2862
};
2963
});
@@ -107,7 +141,7 @@ export const createGetAuth = ({
107141
* }
108142
* ```
109143
*/
110-
export const getAuth = createGetAuth({
144+
export const getAuth = createSyncGetAuth({
111145
debugLoggerName: 'getAuth()',
112146
noAuthStatusMessage: getAuthAuthHeaderMissing(),
113147
});

packages/nextjs/src/server/errors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ Check if signInUrl is missing from your configuration or if it is not an absolut
2020

2121
export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth');
2222

23-
export const authAuthHeaderMissing = (helperName = 'auth') =>
23+
export const authAuthHeaderMissing = (helperName = 'auth', prefixSteps?: string[]) =>
2424
`Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware(). Please ensure the following:
25-
- clerkMiddleware() is used in your Next.js Middleware.
25+
- ${prefixSteps ? [...prefixSteps, ''].join('\n- ') : ' '}clerkMiddleware() is used in your Next.js Middleware.
2626
- Your Middleware matcher is configured to match this route or page.
2727
- If you are using the src directory, make sure the Middleware file is inside of it.
2828

packages/nextjs/src/server/keyless-node.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,47 @@ function removeKeyless() {
209209
unlockFileWriting();
210210
}
211211

212-
export { createOrReadKeyless, removeKeyless };
212+
function hasSrcAppDir() {
213+
const { existsSync } = safeNodeRuntimeFs();
214+
const path = safeNodeRuntimePath();
215+
216+
const projectWithAppSrc = path.join(process.cwd(), 'src', 'app');
217+
218+
return !!existsSync(projectWithAppSrc);
219+
}
220+
221+
function suggestMiddlewareLocation() {
222+
const suggestionMessage = (to?: 'src/', from?: 'src/app/' | 'app/') =>
223+
`Clerk: Move your middleware file to ./${to || ''}middleware.ts. Currently located at ./${from || ''}middleware.ts`;
224+
225+
const { existsSync } = safeNodeRuntimeFs();
226+
const path = safeNodeRuntimePath();
227+
228+
const projectWithAppSrcPath = path.join(process.cwd(), 'src', 'app');
229+
const projectWithAppPath = path.join(process.cwd(), 'app');
230+
231+
if (existsSync(projectWithAppSrcPath)) {
232+
if (existsSync(path.join(projectWithAppSrcPath, 'middleware.ts'))) {
233+
return suggestionMessage('src/', 'src/app/');
234+
}
235+
236+
if (existsSync(path.join(process.cwd(), 'middleware.ts'))) {
237+
return suggestionMessage('src/');
238+
}
239+
240+
// default error
241+
return undefined;
242+
}
243+
244+
if (existsSync(projectWithAppPath)) {
245+
if (existsSync(path.join(projectWithAppPath, 'middleware.ts'))) {
246+
return suggestionMessage(undefined, 'app/');
247+
}
248+
// default error
249+
return undefined;
250+
}
251+
252+
return undefined;
253+
}
254+
255+
export { createOrReadKeyless, removeKeyless, suggestMiddlewareLocation, hasSrcAppDir };
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Discards errors thrown by attempted code
3+
*/
4+
const onlyTry = (cb: () => unknown) => {
5+
try {
6+
cb();
7+
} catch {
8+
// ignore
9+
}
10+
};
11+
12+
export { onlyTry };

0 commit comments

Comments
 (0)