diff --git a/.changeset/selfish-trainers-shop.md b/.changeset/selfish-trainers-shop.md new file mode 100644 index 00000000000..d4da71319c0 --- /dev/null +++ b/.changeset/selfish-trainers-shop.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": patch +--- + +Fix server actions detection diff --git a/packages/nextjs/src/server/nextFetcher.ts b/packages/nextjs/src/server/nextFetcher.ts index 7a73ade19cd..0069dba107d 100644 --- a/packages/nextjs/src/server/nextFetcher.ts +++ b/packages/nextjs/src/server/nextFetcher.ts @@ -12,6 +12,10 @@ type NextFetcher = Fetcher & { * 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 { + /** + * Available for >=next@13.5.4 + * A string with a suffix of `/page` or `/route` dictating usage from a page.tsx or a route.tsx file + */ readonly pagePath?: string; } diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 0d9721aa8cd..4c96d0c53f2 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -110,38 +110,65 @@ export const createProtect = (opts: { }) as AuthProtect; }; +/** + * Detects a request that will trigger a server action + * Can be used from the Edge Middleware and during rendering + */ const isServerActionRequest = (req: Request) => { return ( - !!req.headers.get(nextConstants.Headers.NextUrl) && - (req.headers.get(constants.Headers.Accept)?.includes('text/x-component') || - req.headers.get(constants.Headers.ContentType)?.includes('multipart/form-data') || - !!req.headers.get(nextConstants.Headers.NextAction)) + req.headers.get(constants.Headers.Accept)?.includes('text/x-component') && + !!req.headers.get(nextConstants.Headers.NextAction) ); }; +/** + * Attempts to detect when a request results in a page being displayed + * *Attention*: + * When used within the Edge Middleware this utility will mistakenly detect a Route Handler as a Page + */ const isPageRequest = (req: Request): boolean => { return ( - req.headers.get(constants.Headers.SecFetchDest) === 'document' || - req.headers.get(constants.Headers.SecFetchDest) === 'iframe' || - req.headers.get(constants.Headers.Accept)?.includes('text/html') || - isAppRouterInternalNavigation(req) || - isPagesRouterInternalNavigation(req) + (req.headers.get(constants.Headers.SecFetchDest) === 'document' || + req.headers.get(constants.Headers.SecFetchDest) === 'iframe' || + req.headers.get(constants.Headers.Accept)?.includes('text/html') || + isAppRouterInternalNavigation(req) || + isPagesRouterInternalNavigation(req)) && + !isServerActionRequest(req) && + !isAppRouteRoute(getPagePathAvailable()) ); }; const isAppRouterInternalNavigation = (req: Request) => - (!!req.headers.get(nextConstants.Headers.NextUrl) && !isServerActionRequest(req)) || isPagePathAvailable(); + // Since next@14.2.3 the `next-url` header is being stripped before it can reach the rendering server, and it is only available when executed inside the Next.js Edge Middleware + (!!req.headers.get(nextConstants.Headers.NextUrl) || isAppPageRoute(getPagePathAvailable())) && + !isServerActionRequest(req); + +/** + * Detects usage inside a page.tsx file in App Router + * Found in the Next.js repo + * https://github.com/vercel/next.js/blob/0ac10d79720cc950df96bd9d4958c9be0c075b6f/packages/next/src/lib/is-app-page-route.ts + */ +export function isAppPageRoute(route: string): boolean { + return route.endsWith('/page'); +} -const isPagePathAvailable = () => { +/** + * Detects usage inside a route.tsx file in App Router + * Found in the Next.js repo + * github.com/vercel/next.js/blob/0ac10d79720cc950df96bd9d4958c9be0c075b6f/packages/next/src/lib/is-app-route-route.ts + * In case we want to handle router handlers and server actions differently in the future + */ +export function isAppRouteRoute(route: string): boolean { + return route.endsWith('/route'); +} + +/** + * Returns a string that can either end with `/page` or `/route` indicating that the code run in the context of a page or a route handler. + * These values are only available during rendering (RSC, Route Handlers, Server Actions), and will not be populated when executed inside the Next.js Edge Middleware + */ +const getPagePathAvailable = () => { const __fetch = globalThis.fetch; - return Boolean(isNextFetcher(__fetch) ? __fetch.__nextGetStaticStore().getStore()?.pagePath : false); + return isNextFetcher(__fetch) ? __fetch.__nextGetStaticStore().getStore()?.pagePath || '' : ''; }; 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); -// };