From 8d61d4ae9f3cf4646814eb6238941b2fa4b76f31 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 21 Aug 2024 13:38:56 +0300 Subject: [PATCH 1/3] chore(nextjs): Fix server actions detection `isServerActionRequest` was still depending on the `next-url` header which no longer is available (removed at `14.1.0` for server actions and in `14.2.2` when requesting RSCs. This PR fixes the issue and prepares the ground for future work that would require these utilities to work properly. `pagePath` from __nextGetStaticStore is available since `next@13.5.4` until `next@14.2.5` which is the latest stable release --- packages/nextjs/src/server/protect.ts | 43 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 0d9721aa8c..e711dd1960 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -112,10 +112,8 @@ export const createProtect = (opts: { 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) ); }; @@ -130,18 +128,35 @@ const isPageRequest = (req: Request): boolean => { }; const isAppRouterInternalNavigation = (req: Request) => - (!!req.headers.get(nextConstants.Headers.NextUrl) && !isServerActionRequest(req)) || isPagePathAvailable(); + // The header `next-url` has been dropped since next@14.2.2 + (!!req.headers.get(nextConstants.Headers.NextUrl) || isAppPageRoute(getPagePathAvailable())) && + !isServerActionRequest(req); -const isPagePathAvailable = () => { +/** + * 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'); +} + +/** + * 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. + */ +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); -// }; From 78114120ae06527f5a4f4f2e3cf4e5f13582e3e1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 21 Aug 2024 13:49:09 +0300 Subject: [PATCH 2/3] add changeset --- .changeset/selfish-trainers-shop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/selfish-trainers-shop.md diff --git a/.changeset/selfish-trainers-shop.md b/.changeset/selfish-trainers-shop.md new file mode 100644 index 0000000000..d4da71319c --- /dev/null +++ b/.changeset/selfish-trainers-shop.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": patch +--- + +Fix server actions detection From 128e3c8eb4021dc9fc6e403ae1d208b76e045e14 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 21 Aug 2024 14:47:43 +0300 Subject: [PATCH 3/3] improve docs --- packages/nextjs/src/server/nextFetcher.ts | 4 +++ packages/nextjs/src/server/protect.ts | 30 ++++++++++++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/nextjs/src/server/nextFetcher.ts b/packages/nextjs/src/server/nextFetcher.ts index 7a73ade19c..0069dba107 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 e711dd1960..4c96d0c53f 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -110,6 +110,10 @@ 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(constants.Headers.Accept)?.includes('text/x-component') && @@ -117,18 +121,25 @@ const isServerActionRequest = (req: Request) => { ); }; +/** + * 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) => - // The header `next-url` has been dropped since next@14.2.2 + // 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); @@ -147,12 +158,13 @@ export function isAppPageRoute(route: string): boolean { * 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'); -// } +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;