diff --git a/.changeset/late-falcons-sell.md b/.changeset/late-falcons-sell.md new file mode 100644 index 00000000000..8a03a3494ae --- /dev/null +++ b/.changeset/late-falcons-sell.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Enhance page detection by utilizing the patched fetch from nextjs. diff --git a/integration/tests/protect.test.ts b/integration/tests/protect.test.ts index 3c86aace334..040ab3610ef 100644 --- a/integration/tests/protect.test.ts +++ b/integration/tests/protect.test.ts @@ -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 diff --git a/packages/nextjs/src/server/nextFetcher.ts b/packages/nextjs/src/server/nextFetcher.ts new file mode 100644 index 00000000000..dc74668df65 --- /dev/null +++ b/packages/nextjs/src/server/nextFetcher.ts @@ -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 }; diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 4cadbb859af..f2d957e7463 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -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 }; @@ -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); // };