Skip to content

Commit ed150f0

Browse files
alexcarpenterwobsorianojacekradko
authored
chore(nextjs): Update middleware check for proxy usage (#7269) (#7706)
Co-authored-by: Robert Soriano <sorianorobertc@gmail.com> Co-authored-by: Jacek <jacek@clerk.dev>
1 parent aa2d3b5 commit ed150f0

File tree

7 files changed

+270
-21
lines changed

7 files changed

+270
-21
lines changed

.changeset/tangy-sides-crash.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

integration/tests/middleware-placement.test.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,16 @@ test.describe('next start - missing middleware @quickstart', () => {
7575
});
7676

7777
test('Display error for missing middleware', async ({ page, context }) => {
78+
const { version } = await detectNext(app);
79+
const major = parseSemverMajor(version) ?? 0;
7880
const u = createTestUtils({ app, page, context });
7981
await u.page.goToAppHome();
8082

81-
expect(app.serveOutput).toContain('Your Middleware exists at ./src/middleware.(ts|js)');
83+
const expectedMessage =
84+
major >= 16
85+
? 'Your middleware or proxy file exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
86+
: 'Your middleware file exists at ./src/middleware.(ts|js)';
87+
expect(app.serveOutput).toContain(expectedMessage);
8288
});
8389
});
8490

@@ -109,10 +115,16 @@ test.describe('next start - invalid middleware at root on src/ @quickstart', ()
109115
const u = createTestUtils({ app, page, context });
110116
await u.page.goToAppHome();
111117

112-
expect(app.serveOutput).not.toContain('Your Middleware exists at ./src/middleware.(ts|js)');
113-
expect(app.serveOutput).toContain(
114-
'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./middleware.ts',
115-
);
118+
const expectedMessage =
119+
major >= 16
120+
? 'Your middleware or proxy file exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
121+
: 'Your middleware file exists at ./src/middleware.(ts|js)';
122+
expect(app.serveOutput).not.toContain(expectedMessage);
123+
const expectedError =
124+
major >= 16
125+
? 'Clerk: clerkMiddleware() was not run, your middleware or proxy file might be misplaced. Move your middleware or proxy file to ./src/middleware.ts. Currently located at ./middleware.ts'
126+
: 'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./middleware.ts';
127+
expect(app.serveOutput).toContain(expectedError);
116128
});
117129

118130
test('Does not display misplaced middleware error on Next 16+', async ({ page, context }) => {
@@ -153,10 +165,16 @@ test.describe('next start - invalid middleware inside app on src/ @quickstart',
153165
test.skip(major >= 16 && isCanary, 'Middleware detection is smarter in Next 16 canary.');
154166
const u = createTestUtils({ app, page, context });
155167
await u.page.goToAppHome();
156-
expect(app.serveOutput).not.toContain('Your Middleware exists at ./src/middleware.(ts|js)');
157-
expect(app.serveOutput).toContain(
158-
'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./src/app/middleware.ts',
159-
);
168+
const expectedMessage =
169+
major >= 16
170+
? 'Your middleware or proxy file exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
171+
: 'Your middleware file exists at ./src/middleware.(ts|js)';
172+
expect(app.serveOutput).not.toContain(expectedMessage);
173+
const expectedError =
174+
major >= 16
175+
? 'Clerk: clerkMiddleware() was not run, your middleware or proxy file might be misplaced. Move your middleware or proxy file to ./src/middleware.ts. Currently located at ./src/app/middleware.ts'
176+
: 'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./src/app/middleware.ts';
177+
expect(app.serveOutput).toContain(expectedError);
160178
});
161179

162180
test('Does not display misplaced middleware error on Next 16 canary', async ({ page, context }) => {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { unauthorized } from '../../server/nextErrors';
1111
import type { AuthProtect } from '../../server/protect';
1212
import { createProtect } from '../../server/protect';
1313
import { decryptClerkRequestData } from '../../server/utils';
14+
import { middlewareFileReference } from '../../utils/sdk-versions';
1415
import { buildRequestLike } from './utils';
1516

1617
/**
@@ -76,14 +77,18 @@ export const auth: AuthFn = (async (options?: AuthOptions) => {
7677
const stepsBasedOnSrcDirectory = async () => {
7778
try {
7879
const isSrcAppDir = await import('../../server/fs/middleware-location.js').then(m => m.hasSrcAppDir());
79-
return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}middleware.(ts|js)`];
80+
const fileName =
81+
middlewareFileReference === 'middleware or proxy'
82+
? 'middleware.(ts|js) or proxy.(ts|js)'
83+
: 'middleware.(ts|js)';
84+
return [`Your ${middlewareFileReference} file exists at ./${isSrcAppDir ? 'src/' : ''}${fileName}`];
8085
} catch {
8186
return [];
8287
}
8388
};
8489
const authObject = await createAsyncGetAuth({
8590
debugLoggerName: 'auth()',
86-
noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory()),
91+
noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory(), middlewareFileReference),
8792
})(request, {
8893
treatPendingAsSignedOut: options?.treatPendingAsSignedOut,
8994
acceptsToken: options?.acceptsToken ?? TokenType.SessionToken,

packages/nextjs/src/server/errors.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { middlewareFileReference } from '../utils/sdk-versions';
2+
13
export const missingDomainAndProxy = `
24
Missing domain and proxyUrl. A satellite application needs to specify a domain or a proxyUrl.
35
@@ -18,16 +20,17 @@ Check if signInUrl is missing from your configuration or if it is not an absolut
1820
NEXT_PUBLIC_CLERK_SIGN_IN_URL='SOME_URL'
1921
NEXT_PUBLIC_CLERK_IS_SATELLITE='true'`;
2022

21-
export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth');
23+
export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth', undefined, middlewareFileReference);
2224

23-
export const authAuthHeaderMissing = (helperName = 'auth', prefixSteps?: string[]) =>
24-
`Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware(). Please ensure the following:
25-
- ${prefixSteps ? [...prefixSteps, ''].join('\n- ') : ' '}clerkMiddleware() is used in your Next.js Middleware.
26-
- Your Middleware matcher is configured to match this route or page.
27-
- If you are using the src directory, make sure the Middleware file is inside of it.
25+
export const authAuthHeaderMissing = (helperName = 'auth', prefixSteps?: string[], fileReference = 'middleware') => {
26+
return `Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware(). Please ensure the following:
27+
- ${prefixSteps ? [...prefixSteps, ''].join('\n- ') : ' '}clerkMiddleware() is used in your Next.js ${fileReference} file.
28+
- Your ${fileReference} matcher is configured to match this route or page.
29+
- If you are using the src directory, make sure the ${fileReference} file is inside of it.
2830
2931
For more details, see https://clerk.com/err/auth-middleware
3032
`;
33+
};
3134

3235
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)`;
3336

packages/nextjs/src/server/fs/middleware-location.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isNext16OrHigher, middlewareFileReference } from '../../utils/sdk-versions';
12
import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './utils';
23

34
function hasSrcAppDir() {
@@ -12,12 +13,16 @@ function hasSrcAppDir() {
1213

1314
function suggestMiddlewareLocation() {
1415
const fileExtensions = ['ts', 'js'] as const;
16+
// Next.js 16+ supports both middleware.ts (Edge runtime) and proxy.ts (Node.js runtime)
17+
const fileNames = isNext16OrHigher ? ['middleware', 'proxy'] : ['middleware'];
18+
1519
const suggestionMessage = (
20+
fileName: string,
1621
extension: (typeof fileExtensions)[number],
1722
to: 'src/' | '',
1823
from: 'src/app/' | 'app/' | '',
1924
) =>
20-
`Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./${to}middleware.${extension}. Currently located at ./${from}middleware.${extension}`;
25+
`Clerk: clerkMiddleware() was not run, your ${middlewareFileReference} file might be misplaced. Move your ${middlewareFileReference} file to ./${to}${fileName}.${extension}. Currently located at ./${from}${fileName}.${extension}`;
2126

2227
const { existsSync } = nodeFsOrThrow();
2328
const path = nodePathOrThrow();
@@ -31,9 +36,11 @@ function suggestMiddlewareLocation() {
3136
to: 'src/' | '',
3237
from: 'src/app/' | 'app/' | '',
3338
): string | undefined => {
34-
for (const fileExtension of fileExtensions) {
35-
if (existsSync(path.join(basePath, `middleware.${fileExtension}`))) {
36-
return suggestionMessage(fileExtension, to, from);
39+
for (const fileName of fileNames) {
40+
for (const fileExtension of fileExtensions) {
41+
if (existsSync(path.join(basePath, `${fileName}.${fileExtension}`))) {
42+
return suggestionMessage(fileName, fileExtension, to, from);
43+
}
3744
}
3845
}
3946
return undefined;
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
describe('sdk-versions', () => {
4+
beforeEach(() => {
5+
// Clear module cache to allow re-importing with different mocks
6+
vi.resetModules();
7+
});
8+
9+
describe('meetsNextMinimumVersion', () => {
10+
it('should return true when version meets minimum major version', async () => {
11+
vi.doMock('next/package.json', () => ({
12+
default: { version: '16.0.0' },
13+
}));
14+
15+
const { isNext16OrHigher } = await import('../sdk-versions.js');
16+
expect(isNext16OrHigher).toBe(true);
17+
});
18+
19+
it('should return true when version exceeds minimum major version', async () => {
20+
vi.doMock('next/package.json', () => ({
21+
default: { version: '17.0.0' },
22+
}));
23+
24+
const { isNext16OrHigher } = await import('../sdk-versions.js');
25+
expect(isNext16OrHigher).toBe(true);
26+
});
27+
28+
it('should return false when version is below minimum major version', async () => {
29+
vi.doMock('next/package.json', () => ({
30+
default: { version: '15.9.9' },
31+
}));
32+
33+
const { isNext16OrHigher } = await import('../sdk-versions.js');
34+
expect(isNext16OrHigher).toBe(false);
35+
});
36+
37+
it('should return false when version is exactly one below minimum', async () => {
38+
vi.doMock('next/package.json', () => ({
39+
default: { version: '15.0.0' },
40+
}));
41+
42+
const { isNext16OrHigher } = await import('../sdk-versions.js');
43+
expect(isNext16OrHigher).toBe(false);
44+
});
45+
46+
it('should handle patch versions correctly', async () => {
47+
vi.doMock('next/package.json', () => ({
48+
default: { version: '16.5.3' },
49+
}));
50+
51+
const { isNext16OrHigher } = await import('../sdk-versions.js');
52+
expect(isNext16OrHigher).toBe(true);
53+
});
54+
55+
it('should handle beta/prerelease versions correctly', async () => {
56+
vi.doMock('next/package.json', () => ({
57+
default: { version: '16.0.0-beta.1' },
58+
}));
59+
60+
const { isNext16OrHigher } = await import('../sdk-versions.js');
61+
expect(isNext16OrHigher).toBe(true);
62+
});
63+
64+
it('should return false when version is missing', async () => {
65+
vi.doMock('next/package.json', () => ({
66+
default: {},
67+
}));
68+
69+
const { isNext16OrHigher } = await import('../sdk-versions.js');
70+
expect(isNext16OrHigher).toBe(false);
71+
});
72+
73+
it('should return false when version is null', async () => {
74+
vi.doMock('next/package.json', () => ({
75+
default: { version: null },
76+
}));
77+
78+
const { isNext16OrHigher } = await import('../sdk-versions.js');
79+
expect(isNext16OrHigher).toBe(false);
80+
});
81+
82+
it('should return false when version is undefined', async () => {
83+
vi.doMock('next/package.json', () => ({
84+
default: { version: undefined },
85+
}));
86+
87+
const { isNext16OrHigher } = await import('../sdk-versions.js');
88+
expect(isNext16OrHigher).toBe(false);
89+
});
90+
91+
it('should return false when version is an empty string', async () => {
92+
vi.doMock('next/package.json', () => ({
93+
default: { version: '' },
94+
}));
95+
96+
const { isNext16OrHigher } = await import('../sdk-versions.js');
97+
expect(isNext16OrHigher).toBe(false);
98+
});
99+
100+
it('should return false when version cannot be parsed as a number', async () => {
101+
vi.doMock('next/package.json', () => ({
102+
default: { version: 'invalid-version' },
103+
}));
104+
105+
const { isNext16OrHigher } = await import('../sdk-versions.js');
106+
expect(isNext16OrHigher).toBe(false);
107+
});
108+
109+
it('should handle single-digit major versions', async () => {
110+
vi.doMock('next/package.json', () => ({
111+
default: { version: '9.0.0' },
112+
}));
113+
114+
const { isNext16OrHigher } = await import('../sdk-versions.js');
115+
expect(isNext16OrHigher).toBe(false);
116+
});
117+
118+
it('should handle double-digit major versions', async () => {
119+
vi.doMock('next/package.json', () => ({
120+
default: { version: '20.0.0' },
121+
}));
122+
123+
const { isNext16OrHigher } = await import('../sdk-versions.js');
124+
expect(isNext16OrHigher).toBe(true);
125+
});
126+
127+
it('should handle version strings with leading zeros', async () => {
128+
vi.doMock('next/package.json', () => ({
129+
default: { version: '016.0.0' },
130+
}));
131+
132+
const { isNext16OrHigher } = await import('../sdk-versions.js');
133+
expect(isNext16OrHigher).toBe(true);
134+
});
135+
});
136+
137+
describe('isNext16OrHigher', () => {
138+
it('should be a boolean value', async () => {
139+
vi.doMock('next/package.json', () => ({
140+
default: { version: '16.0.0' },
141+
}));
142+
143+
const { isNext16OrHigher } = await import('../sdk-versions.js');
144+
expect(typeof isNext16OrHigher).toBe('boolean');
145+
});
146+
147+
it('should correctly identify Next.js 16', async () => {
148+
vi.doMock('next/package.json', () => ({
149+
default: { version: '16.0.0' },
150+
}));
151+
152+
const { isNext16OrHigher } = await import('../sdk-versions.js');
153+
expect(isNext16OrHigher).toBe(true);
154+
});
155+
156+
it('should correctly identify Next.js 15 as not 16+', async () => {
157+
vi.doMock('next/package.json', () => ({
158+
default: { version: '15.2.3' },
159+
}));
160+
161+
const { isNext16OrHigher } = await import('../sdk-versions.js');
162+
expect(isNext16OrHigher).toBe(false);
163+
});
164+
});
165+
166+
describe('middlewareFileReference', () => {
167+
it('should return "middleware or proxy" for Next.js 16+', async () => {
168+
vi.doMock('next/package.json', () => ({
169+
default: { version: '16.0.0' },
170+
}));
171+
172+
const { middlewareFileReference } = await import('../sdk-versions.js');
173+
expect(middlewareFileReference).toBe('middleware or proxy');
174+
});
175+
176+
it('should return "middleware" for Next.js < 16', async () => {
177+
vi.doMock('next/package.json', () => ({
178+
default: { version: '15.9.9' },
179+
}));
180+
181+
const { middlewareFileReference } = await import('../sdk-versions.js');
182+
expect(middlewareFileReference).toBe('middleware');
183+
});
184+
185+
it('should return "middleware or proxy" for Next.js 17+', async () => {
186+
vi.doMock('next/package.json', () => ({
187+
default: { version: '17.0.0' },
188+
}));
189+
190+
const { middlewareFileReference } = await import('../sdk-versions.js');
191+
expect(middlewareFileReference).toBe('middleware or proxy');
192+
});
193+
});
194+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import nextPkg from 'next/package.json';
2+
3+
function meetsNextMinimumVersion(minimumMajorVersion: number) {
4+
if (!nextPkg?.version) {
5+
return false;
6+
}
7+
8+
const majorVersion = parseInt(nextPkg.version.split('.')[0], 10);
9+
return !isNaN(majorVersion) && majorVersion >= minimumMajorVersion;
10+
}
11+
12+
/**
13+
* Next.js 16+ supports proxy.ts (Node.js runtime) as an alternative to middleware.ts (Edge runtime)
14+
*/
15+
export const isNext16OrHigher = meetsNextMinimumVersion(16);
16+
17+
/**
18+
* Display name for middleware/proxy file references in error messages
19+
*/
20+
export const middlewareFileReference = isNext16OrHigher ? 'middleware or proxy' : 'middleware';

0 commit comments

Comments
 (0)