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
2 changes: 2 additions & 0 deletions .changeset/tangy-sides-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
38 changes: 29 additions & 9 deletions integration/tests/middleware-placement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 or proxy file exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
: 'Your middleware file exists at ./src/middleware.(ts|js)';
expect(app.serveOutput).toContain(expectedMessage);
});
});

Expand Down Expand Up @@ -105,10 +111,16 @@ 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)');
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 expectedMessage =
major >= 16
? 'Your middleware or proxy file exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
: 'Your middleware file exists at ./src/middleware.(ts|js)';
expect(app.serveOutput).not.toContain(expectedMessage);
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 }) => {
Expand Down Expand Up @@ -142,11 +154,19 @@ 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)');
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 expectedMessage =
major >= 16
? 'Your middleware or proxy file exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
: 'Your middleware file exists at ./src/middleware.(ts|js)';
expect(app.serveOutput).not.toContain(expectedMessage);
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);
});
});
9 changes: 7 additions & 2 deletions packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { unauthorized } from '../../server/nextErrors';
import type { AuthProtect } from '../../server/protect';
import { createProtect } from '../../server/protect';
import { decryptClerkRequestData } from '../../server/utils';
import { middlewareFileReference } from '../../utils/sdk-versions';
import { buildRequestLike } from './utils';

/**
Expand Down Expand Up @@ -76,14 +77,18 @@ export const auth: AuthFn = (async (options?: AuthOptions) => {
const stepsBasedOnSrcDirectory = async () => {
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 =
middlewareFileReference === 'middleware or proxy'
? 'middleware.(ts|js) or proxy.(ts|js)'
: 'middleware.(ts|js)';
return [`Your ${middlewareFileReference} file exists at ./${isSrcAppDir ? 'src/' : ''}${fileName}`];
} catch {
return [];
}
};
const authObject = await createAsyncGetAuth({
debugLoggerName: 'auth()',
noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory()),
noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory(), middlewareFileReference),
})(request, {
treatPendingAsSignedOut: options?.treatPendingAsSignedOut,
acceptsToken: options?.acceptsToken ?? TokenType.SessionToken,
Expand Down
15 changes: 9 additions & 6 deletions packages/nextjs/src/server/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { middlewareFileReference } from '../utils/sdk-versions';

export const missingDomainAndProxy = `
Missing domain and proxyUrl. A satellite application needs to specify a domain or a proxyUrl.

Expand All @@ -18,16 +20,17 @@ Check if signInUrl is missing from your configuration or if it is not an absolut
NEXT_PUBLIC_CLERK_SIGN_IN_URL='SOME_URL'
NEXT_PUBLIC_CLERK_IS_SATELLITE='true'`;

export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth');
export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth', undefined, middlewareFileReference);

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

For more details, see https://clerk.com/err/auth-middleware
`;
};

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

Expand Down
15 changes: 11 additions & 4 deletions packages/nextjs/src/server/fs/middleware-location.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isNext16OrHigher, middlewareFileReference } from '../../utils/sdk-versions';
import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './utils';

function hasSrcAppDir() {
Expand All @@ -12,12 +13,16 @@ 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 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 ${middlewareFileReference} file might be misplaced. Move your ${middlewareFileReference} file to ./${to}${fileName}.${extension}. Currently located at ./${from}${fileName}.${extension}`;

const { existsSync } = nodeFsOrThrow();
const path = nodePathOrThrow();
Expand All @@ -31,9 +36,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;
Expand Down
194 changes: 194 additions & 0 deletions packages/nextjs/src/utils/__tests__/sdk-versions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
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);
});
});

describe('middlewareFileReference', () => {
it('should return "middleware or proxy" for Next.js 16+', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '16.0.0' },
}));

const { middlewareFileReference } = await import('../sdk-versions.js');
expect(middlewareFileReference).toBe('middleware or proxy');
});

it('should return "middleware" for Next.js < 16', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '15.9.9' },
}));

const { middlewareFileReference } = await import('../sdk-versions.js');
expect(middlewareFileReference).toBe('middleware');
});

it('should return "middleware or proxy" for Next.js 17+', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '17.0.0' },
}));

const { middlewareFileReference } = await import('../sdk-versions.js');
expect(middlewareFileReference).toBe('middleware or proxy');
});
});
});
30 changes: 30 additions & 0 deletions packages/nextjs/src/utils/sdk-versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import nextPkg from 'next/package.json';

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') ?? false);

/**
* Next.js 16+ supports proxy.ts (Node.js runtime) as an alternative to middleware.ts (Edge runtime)
*/
const isNext16OrHigher = meetsNextMinimumVersion(16);

/**
* Display name for middleware/proxy file references in error messages
*/
export const middlewareFileReference = isNext16OrHigher ? 'middleware or proxy' : 'middleware';

export { isNext13, isNextWithUnstableServerActions, isNext16OrHigher };
Loading