Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/late-falcons-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/nextjs': patch
---

Enhance page detection by utilizing the patched fetch from nextjs.
17 changes: 7 additions & 10 deletions integration/tests/protect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,13 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz
test('Protect in RSCs and RCCs as `signed-out user`', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

// Do not run this part for nextjs v14, the flow is broken in 14.2.3 because vercel removed a header that our page detection logic depends on
if (!u.nexJsVersion.startsWith('14')) {
/**
* Soft navigations
*/
await u.page.goToRelative('/');
await page.getByText('Page Protected').click();
await page.waitForURL('**/sign-in?**');
await u.po.signIn.waitForMounted();
}
/**
* Soft navigations
*/
await u.page.goToRelative('/');
await page.getByText('Page Protected').click();
await page.waitForURL('**/sign-in?**');
await u.po.signIn.waitForMounted();

/**
* Hard navigations
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/server/nextFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type Fetcher = typeof globalThis.fetch;

/**
* Based on nextjs internal implementation https://github.com/vercel/next.js/blob/6185444e0a944a82e7719ac37dad8becfed86acd/packages/next/src/server/lib/patch-fetch.ts#L23
*/
type NextFetcher = Fetcher & {
readonly __nextPatched: true;
readonly __nextGetStaticStore: () => { getStore: () => StaticGenerationAsyncStorage };
};

/**
* Full type can be found https://github.com/vercel/next.js/blob/6185444e0a944a82e7719ac37dad8becfed86acd/packages/next/src/client/components/static-generation-async-storage.external.ts#L4
*/
interface StaticGenerationAsyncStorage {
readonly pagePath?: string;
}

function isNextFetcher(fetch: Fetcher | NextFetcher): fetch is NextFetcher {
return '__nextPatched' in fetch && fetch.__nextPatched === true;
}

export { isNextFetcher };
23 changes: 18 additions & 5 deletions packages/nextjs/src/server/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from '@clerk/types';

import { constants as nextConstants } from '../constants';
import { isNextFetcher } from './nextFetcher';

type AuthProtectOptions = { unauthorizedUrl?: string; unauthenticatedUrl?: string };

Expand Down Expand Up @@ -121,12 +122,24 @@ const isPageRequest = (req: Request): boolean => {
return (
req.headers.get(constants.Headers.SecFetchDest) === 'document' ||
req.headers.get(constants.Headers.Accept)?.includes('text/html') ||
(!!req.headers.get(nextConstants.Headers.NextUrl) && !isServerActionRequest(req)) ||
!!req.headers.get(nextConstants.Headers.NextjsData)
isAppRouterInternalNavigation(req) ||
isPagesRouterInternalNavigation(req)
);
};

// In case we want to handle router handlers and server actions differently in the future
// const isRouteHandler = (req: Request) => {
// return !isPageRequest(req) && !isServerAction(req);
const isAppRouterInternalNavigation = (req: Request) =>
(!!req.headers.get(nextConstants.Headers.NextUrl) && !isServerActionRequest(req)) || isPagePathAvailable();

const isPagePathAvailable = () => {
const __fetch = globalThis.fetch;
return Boolean(isNextFetcher(__fetch) ? __fetch.__nextGetStaticStore().getStore().pagePath : false);
};

const isPagesRouterInternalNavigation = (req: Request) => !!req.headers.get(nextConstants.Headers.NextjsData);

// /**
// * In case we want to handle router handlers and server actions differently in the future
// */
// const isApiRouteRequest = (req: Request) => {
// return !isPageRequest(req) && !isServerActionRequest(req);
// };