Skip to content

Commit ad356bf

Browse files
authored
feat(nextjs): [improved] Hint correct middleware location when missing clerkMiddleware (#5028)
1 parent 8602902 commit ad356bf

File tree

15 files changed

+198
-65
lines changed

15 files changed

+198
-65
lines changed

.changeset/orange-clouds-relax.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

.changeset/weak-phones-retire.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+
Bug fix: Remove warning for accessing Node APIs when running `next build` with `clerkMiddleware` imported.

packages/nextjs/src/app-router/keyless-actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,6 @@ export async function deleteKeylessAction() {
7272
return;
7373
}
7474

75-
await import('../server/keyless-node.js').then(m => m.removeKeyless());
75+
await import('../server/keyless-node.js').then(m => m.removeKeyless()).catch(() => {});
7676
return;
7777
}

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

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

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

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/fs/middleware-location.js').then(m => m.hasSrcAppDir());
74+
return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}middleware.(ts|js)`];
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/runtime/browser/safe-node-apis.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
*/
44
const fs = undefined;
55
const path = undefined;
6+
const cwd = undefined;
67

7-
module.exports = { fs, path };
8+
module.exports = { fs, path, cwd };

packages/nextjs/src/runtime/node/safe-node-apis.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ const fs = {
1414
rmSync,
1515
};
1616

17-
module.exports = { fs, path };
17+
const cwd = () => process.cwd();
18+
19+
module.exports = { fs, path, cwd };

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: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,60 @@ import { constants } from '@clerk/backend/internal';
33
import { isTruthy } from '@clerk/shared/underscore';
44

55
import { withLogger } from '../utils/debugLogger';
6+
import { isNextWithUnstableServerActions } from '../utils/sdk-versions';
67
import { getAuthDataFromRequest } from './data/getAuthDataFromRequest';
78
import { getAuthAuthHeaderMissing } from './errors';
8-
import { getHeader } from './headers-utils';
9+
import { detectClerkMiddleware, getHeader } from './headers-utils';
910
import type { RequestLike } from './types';
1011
import { assertAuthStatus } from './utils';
1112

12-
export const createGetAuth = ({
13+
/**
14+
* The async variant of our old `createGetAuth` allows for asynchronous code inside its callback.
15+
* Should be used with function like `auth()` that are already asynchronous.
16+
*/
17+
export const createAsyncGetAuth = ({
18+
debugLoggerName,
1319
noAuthStatusMessage,
20+
}: {
21+
debugLoggerName: string;
22+
noAuthStatusMessage: string;
23+
}) =>
24+
withLogger(debugLoggerName, logger => {
25+
return async (req: RequestLike, opts?: { secretKey?: string }): Promise<AuthObject> => {
26+
if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) {
27+
logger.enable();
28+
}
29+
30+
if (!detectClerkMiddleware(req)) {
31+
// Keep the same behaviour for versions that may have issues with bundling `node:fs`
32+
if (isNextWithUnstableServerActions) {
33+
assertAuthStatus(req, noAuthStatusMessage);
34+
}
35+
36+
const missConfiguredMiddlewareLocation = await import('./fs/middleware-location.js')
37+
.then(m => m.suggestMiddlewareLocation())
38+
.catch(() => undefined);
39+
40+
if (missConfiguredMiddlewareLocation) {
41+
throw new Error(missConfiguredMiddlewareLocation);
42+
}
43+
44+
// still throw there is no suggested move location
45+
assertAuthStatus(req, noAuthStatusMessage);
46+
}
47+
48+
return getAuthDataFromRequest(req, { ...opts, logger });
49+
};
50+
});
51+
52+
/**
53+
* Previous known as `createGetAuth`. We needed to create a sync and async variant in order to allow for improvements
54+
* that required dynamic imports (using `require` would not work).
55+
* It powers the synchronous top-level api `getAuh()`.
56+
*/
57+
export const createSyncGetAuth = ({
1458
debugLoggerName,
59+
noAuthStatusMessage,
1560
}: {
1661
debugLoggerName: string;
1762
noAuthStatusMessage: string;
@@ -23,7 +68,6 @@ export const createGetAuth = ({
2368
}
2469

2570
assertAuthStatus(req, noAuthStatusMessage);
26-
2771
return getAuthDataFromRequest(req, { ...opts, logger });
2872
};
2973
});
@@ -107,7 +151,7 @@ export const createGetAuth = ({
107151
* }
108152
* ```
109153
*/
110-
export const getAuth = createGetAuth({
154+
export const getAuth = createSyncGetAuth({
111155
debugLoggerName: 'getAuth()',
112156
noAuthStatusMessage: getAuthAuthHeaderMissing(),
113157
});

0 commit comments

Comments
 (0)