From a8ba666cbbabd6433e8966dae288d75987f89ea4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 20 Nov 2025 11:37:09 -0500 Subject: [PATCH 1/5] chore(nextjs): Update middleware check for proxy usage --- packages/nextjs/src/app-router/server/auth.ts | 5 +++-- .../nextjs/src/server/fs/middleware-location.ts | 16 ++++++++++++---- packages/nextjs/src/utils/sdk-versions.ts | 7 ++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 3a4f0efd544..2a332e44162 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -11,7 +11,7 @@ import { unauthorized } from '../../server/nextErrors'; import type { AuthProtect } from '../../server/protect'; import { createProtect } from '../../server/protect'; import { decryptClerkRequestData } from '../../server/utils'; -import { isNextWithUnstableServerActions } from '../../utils/sdk-versions'; +import { isNext16OrHigher, isNextWithUnstableServerActions } from '../../utils/sdk-versions'; import { buildRequestLike } from './utils'; /** @@ -81,7 +81,8 @@ export const auth: AuthFn = (async (options?: AuthOptions) => { try { const isSrcAppDir = await import('../../server/fs/middleware-location.js').then(m => m.hasSrcAppDir()); - return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}middleware.(ts|js)`]; + const fileName = isNext16OrHigher ? 'middleware.(ts|js) or proxy.(ts|js)' : 'middleware.(ts|js)'; + return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}${fileName}`]; } catch { return []; } diff --git a/packages/nextjs/src/server/fs/middleware-location.ts b/packages/nextjs/src/server/fs/middleware-location.ts index 3586d4a1ae2..67a7393e20d 100644 --- a/packages/nextjs/src/server/fs/middleware-location.ts +++ b/packages/nextjs/src/server/fs/middleware-location.ts @@ -1,3 +1,4 @@ +import { isNext16OrHigher } from '../../utils/sdk-versions'; import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './utils'; function hasSrcAppDir() { @@ -12,12 +13,17 @@ function hasSrcAppDir() { function suggestMiddlewareLocation() { const fileExtensions = ['ts', 'js'] as const; + // Next.js 16+ supports both middleware.ts (Edge runtime) and proxy.ts (Node.js runtime) + const fileNames = isNext16OrHigher ? ['middleware', 'proxy'] : ['middleware']; + const fileNameDisplay = isNext16OrHigher ? 'middleware or proxy' : 'middleware'; + const suggestionMessage = ( + fileName: string, extension: (typeof fileExtensions)[number], to: 'src/' | '', from: 'src/app/' | 'app/' | '', ) => - `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}`; + `Clerk: clerkMiddleware() was not run, your ${fileNameDisplay} file might be misplaced. Move your ${fileNameDisplay} file to ./${to}${fileName}.${extension}. Currently located at ./${from}${fileName}.${extension}`; const { existsSync } = nodeFsOrThrow(); const path = nodePathOrThrow(); @@ -31,9 +37,11 @@ function suggestMiddlewareLocation() { to: 'src/' | '', from: 'src/app/' | 'app/' | '', ): string | undefined => { - for (const fileExtension of fileExtensions) { - if (existsSync(path.join(basePath, `middleware.${fileExtension}`))) { - return suggestionMessage(fileExtension, to, from); + for (const fileName of fileNames) { + for (const fileExtension of fileExtensions) { + if (existsSync(path.join(basePath, `${fileName}.${fileExtension}`))) { + return suggestionMessage(fileName, fileExtension, to, from); + } } } return undefined; diff --git a/packages/nextjs/src/utils/sdk-versions.ts b/packages/nextjs/src/utils/sdk-versions.ts index 729a096a6f9..e470186ca07 100644 --- a/packages/nextjs/src/utils/sdk-versions.ts +++ b/packages/nextjs/src/utils/sdk-versions.ts @@ -8,4 +8,9 @@ const isNext13 = nextPkg.version.startsWith('13.'); */ const isNextWithUnstableServerActions = isNext13 || nextPkg.version.startsWith('14.0'); -export { isNext13, isNextWithUnstableServerActions }; +/** + * Next.js 16+ renamed middleware.ts to proxy.ts + */ +const isNext16OrHigher = nextPkg.version.startsWith('16.') || parseInt(nextPkg.version.split('.')[0]) >= 16; + +export { isNext13, isNextWithUnstableServerActions, isNext16OrHigher }; From 26f15d66a19d0151039f43dce5d91b29bbee3ee6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 20 Nov 2025 11:40:51 -0500 Subject: [PATCH 2/5] add changeset --- .changeset/tangy-sides-crash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tangy-sides-crash.md diff --git a/.changeset/tangy-sides-crash.md b/.changeset/tangy-sides-crash.md new file mode 100644 index 00000000000..318a91808ce --- /dev/null +++ b/.changeset/tangy-sides-crash.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Updates middleware location check to account for proxy.ts in next 16+ applications. From ad02482a549ff2b761667ac61b0004118b20dea9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 5 Jan 2026 11:31:40 -0500 Subject: [PATCH 3/5] add tests --- .../src/utils/__tests__/sdk-versions.test.ts | 165 ++++++++++++++++++ packages/nextjs/src/utils/sdk-versions.ts | 15 +- 2 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 packages/nextjs/src/utils/__tests__/sdk-versions.test.ts diff --git a/packages/nextjs/src/utils/__tests__/sdk-versions.test.ts b/packages/nextjs/src/utils/__tests__/sdk-versions.test.ts new file mode 100644 index 00000000000..c37be227534 --- /dev/null +++ b/packages/nextjs/src/utils/__tests__/sdk-versions.test.ts @@ -0,0 +1,165 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('sdk-versions', () => { + beforeEach(() => { + // Clear module cache to allow re-importing with different mocks + vi.resetModules(); + }); + + describe('meetsNextMinimumVersion', () => { + it('should return true when version meets minimum major version', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '16.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(true); + }); + + it('should return true when version exceeds minimum major version', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '17.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(true); + }); + + it('should return false when version is below minimum major version', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '15.9.9' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should return false when version is exactly one below minimum', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '15.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should handle patch versions correctly', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '16.5.3' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(true); + }); + + it('should handle beta/prerelease versions correctly', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '16.0.0-beta.1' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(true); + }); + + it('should return false when version is missing', async () => { + vi.doMock('next/package.json', () => ({ + default: {}, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should return false when version is null', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: null }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should return false when version is undefined', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: undefined }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should return false when version is an empty string', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should return false when version cannot be parsed as a number', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: 'invalid-version' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should handle single-digit major versions', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '9.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + + it('should handle double-digit major versions', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '20.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(true); + }); + + it('should handle version strings with leading zeros', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '016.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(true); + }); + }); + + describe('isNext16OrHigher', () => { + it('should be a boolean value', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '16.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(typeof isNext16OrHigher).toBe('boolean'); + }); + + it('should correctly identify Next.js 16', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '16.0.0' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(true); + }); + + it('should correctly identify Next.js 15 as not 16+', async () => { + vi.doMock('next/package.json', () => ({ + default: { version: '15.2.3' }, + })); + + const { isNext16OrHigher } = await import('../sdk-versions.js'); + expect(isNext16OrHigher).toBe(false); + }); + }); +}); diff --git a/packages/nextjs/src/utils/sdk-versions.ts b/packages/nextjs/src/utils/sdk-versions.ts index e470186ca07..2dc49c1698e 100644 --- a/packages/nextjs/src/utils/sdk-versions.ts +++ b/packages/nextjs/src/utils/sdk-versions.ts @@ -1,16 +1,25 @@ import nextPkg from 'next/package.json'; -const isNext13 = nextPkg.version.startsWith('13.'); +function meetsNextMinimumVersion(minimumMajorVersion: number) { + if (!nextPkg?.version) { + return false; + } + + const majorVersion = parseInt(nextPkg.version.split('.')[0], 10); + return !isNaN(majorVersion) && majorVersion >= minimumMajorVersion; +} + +const isNext13 = nextPkg?.version?.startsWith('13.') ?? false; /** * Those versions are affected by a bundling issue that will break the application if `node:fs` is used inside a server function. * The affected versions are >=next@13.5.4 and <=next@14.0.4 */ -const isNextWithUnstableServerActions = isNext13 || nextPkg.version.startsWith('14.0'); +const isNextWithUnstableServerActions = isNext13 || (nextPkg?.version?.startsWith('14.0') ?? false); /** * Next.js 16+ renamed middleware.ts to proxy.ts */ -const isNext16OrHigher = nextPkg.version.startsWith('16.') || parseInt(nextPkg.version.split('.')[0]) >= 16; +const isNext16OrHigher = meetsNextMinimumVersion(16); export { isNext13, isNextWithUnstableServerActions, isNext16OrHigher }; From b5c443f2fa7fb1de31c522c0da2f17e5920bfa79 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 5 Jan 2026 13:36:52 -0500 Subject: [PATCH 4/5] Update middleware-placement.test.ts --- .../tests/middleware-placement.test.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/integration/tests/middleware-placement.test.ts b/integration/tests/middleware-placement.test.ts index 42bc1b7227f..19861c59862 100644 --- a/integration/tests/middleware-placement.test.ts +++ b/integration/tests/middleware-placement.test.ts @@ -71,10 +71,16 @@ test.describe('next start - missing middleware @quickstart', () => { }); test('Display error for missing middleware', async ({ page, context }) => { + const { version } = await detectNext(app); + const major = parseSemverMajor(version) ?? 0; const u = createTestUtils({ app, page, context }); await u.page.goToAppHome(); - expect(app.serveOutput).toContain('Your Middleware exists at ./src/middleware.(ts|js)'); + const expectedMessage = + major >= 16 + ? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)' + : 'Your Middleware exists at ./src/middleware.(ts|js)'; + expect(app.serveOutput).toContain(expectedMessage); }); }); @@ -105,7 +111,11 @@ test.describe('next start - invalid middleware at root on src/ @quickstart', () const u = createTestUtils({ app, page, context }); await u.page.goToAppHome(); - expect(app.serveOutput).not.toContain('Your Middleware exists at ./src/middleware.(ts|js)'); + const expectedMessage = + major >= 16 + ? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)' + : 'Your Middleware exists at ./src/middleware.(ts|js)'; + expect(app.serveOutput).not.toContain(expectedMessage); expect(app.serveOutput).toContain( 'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./middleware.ts', ); @@ -142,9 +152,15 @@ test.describe('next start - invalid middleware inside app on src/ @quickstart', page, context, }) => { + const { version } = await detectNext(app); + const major = parseSemverMajor(version) ?? 0; const u = createTestUtils({ app, page, context }); await u.page.goToAppHome(); - expect(app.serveOutput).not.toContain('Your Middleware exists at ./src/middleware.(ts|js)'); + const expectedMessage = + major >= 16 + ? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)' + : 'Your Middleware exists at ./src/middleware.(ts|js)'; + expect(app.serveOutput).not.toContain(expectedMessage); expect(app.serveOutput).toContain( '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', ); From aace04136830e778e92bf26e1c87ed2eacafa9de Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 5 Jan 2026 13:59:30 -0500 Subject: [PATCH 5/5] Update middleware-placement.test.ts --- integration/tests/middleware-placement.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/integration/tests/middleware-placement.test.ts b/integration/tests/middleware-placement.test.ts index 19861c59862..c756d83374a 100644 --- a/integration/tests/middleware-placement.test.ts +++ b/integration/tests/middleware-placement.test.ts @@ -116,9 +116,11 @@ test.describe('next start - invalid middleware at root on src/ @quickstart', () ? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)' : 'Your Middleware exists at ./src/middleware.(ts|js)'; expect(app.serveOutput).not.toContain(expectedMessage); - expect(app.serveOutput).toContain( - 'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./middleware.ts', - ); + const expectedError = + major >= 16 + ? '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' + : 'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./middleware.ts'; + expect(app.serveOutput).toContain(expectedError); }); test('Does not display misplaced middleware error on Next 16+', async ({ page, context }) => { @@ -161,8 +163,10 @@ test.describe('next start - invalid middleware inside app on src/ @quickstart', ? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)' : 'Your Middleware exists at ./src/middleware.(ts|js)'; expect(app.serveOutput).not.toContain(expectedMessage); - expect(app.serveOutput).toContain( - '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', - ); + const expectedError = + major >= 16 + ? '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' + : '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'; + expect(app.serveOutput).toContain(expectedError); }); });