From 152c9b20e02bf85bd7182a60b500ac8969f62de1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 28 Mar 2024 16:54:33 -0600 Subject: [PATCH] fix: ensure that the urlPathname is always a pathname --- .../next-core/src/next_app/app_page_entry.rs | 1 - .../next-core/src/next_app/app_route_entry.rs | 3 +- packages/next/src/build/templates/app-page.ts | 1 - .../next/src/build/templates/app-route.ts | 5 +- packages/next/src/build/utils.ts | 3 +- .../build/webpack/loaders/next-app-loader.ts | 2 - .../request-async-storage.external.ts | 17 +++++ ...tatic-generation-async-storage.external.ts | 7 +- packages/next/src/export/routes/app-route.ts | 3 +- packages/next/src/export/worker.ts | 1 - packages/next/src/lib/metadata/metadata.tsx | 4 +- packages/next/src/lib/url.ts | 10 +-- .../next/src/server/after/after-context.ts | 1 + packages/next/src/server/after/after.ts | 4 +- .../next/src/server/app-render/app-render.tsx | 31 ++++---- .../app-render/create-component-tree.tsx | 7 +- .../server/app-render/dynamic-rendering.ts | 28 +++---- .../next/src/server/app-render/entry-base.ts | 2 +- packages/next/src/server/app-render/types.ts | 1 - .../src/server/app-render/validate-url.tsx | 18 ----- .../request-async-storage-wrapper.ts | 41 +++++++--- ...static-generation-async-storage-wrapper.ts | 14 ++-- packages/next/src/server/base-server.ts | 2 - packages/next/src/server/lib/patch-fetch.ts | 75 +++++++++---------- packages/next/src/server/load-components.ts | 2 +- .../server/load-default-error-components.ts | 2 +- .../server/route-modules/app-route/module.ts | 23 +++--- packages/next/src/server/web/adapter.ts | 14 ++-- .../server/web/spec-extension/revalidate.ts | 5 +- .../web/spec-extension/unstable-cache.ts | 70 ++++++++++------- .../router/utils/parse-relative-url.test.ts | 20 +++++ .../lib/router/utils/parse-relative-url.ts | 21 +++++- .../lib/router/utils/resolve-rewrites.ts | 4 +- test/unit/validate-url.test.ts | 13 ---- 34 files changed, 238 insertions(+), 217 deletions(-) delete mode 100644 packages/next/src/server/app-render/validate-url.tsx create mode 100644 packages/next/src/shared/lib/router/utils/parse-relative-url.test.ts delete mode 100644 test/unit/validate-url.test.ts diff --git a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs index 1305d4346ca58..9beb79635d156 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs @@ -81,7 +81,6 @@ pub async fn get_app_page_entry( indexmap! { "VAR_DEFINITION_PAGE" => page.to_string().into(), "VAR_DEFINITION_PATHNAME" => pathname.clone(), - "VAR_ORIGINAL_PATHNAME" => original_name.clone(), // TODO(alexkirsz) Support custom global error. "VAR_MODULE_GLOBAL_ERROR" => "next/dist/client/components/error-boundary".into(), }, diff --git a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs index 1114043a4bed1..06aa3e74392c4 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs @@ -84,8 +84,7 @@ pub async fn get_app_route_entry( "VAR_DEFINITION_PATHNAME" => pathname.clone(), "VAR_DEFINITION_FILENAME" => path.file_stem().await?.as_ref().unwrap().as_str().into(), // TODO(alexkirsz) Is this necessary? - "VAR_DEFINITION_BUNDLE_PATH" => "".into(), - "VAR_ORIGINAL_PATHNAME" => original_name.clone(), + "VAR_DEFINITION_BUNDLE_PATH" => "".to_string().into(), "VAR_RESOLVED_PAGE_PATH" => path.to_string().await?.clone_value(), "VAR_USERLAND" => INNER.into(), }, diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index a3594095494c7..0fb72182245ed 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -27,7 +27,6 @@ declare const __next_app_load_chunk__: any // INJECT:__next_app_require__ // INJECT:__next_app_load_chunk__ -export const originalPathname = 'VAR_ORIGINAL_PATHNAME' export const __next_app__ = { require: __next_app_require__, loadChunk: __next_app_load_chunk__, diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index b65276d0c84f2..81217489f2d98 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -35,10 +35,8 @@ const routeModule = new AppRouteRouteModule({ const { requestAsyncStorage, staticGenerationAsyncStorage, serverHooks } = routeModule -const originalPathname = 'VAR_ORIGINAL_PATHNAME' - function patchFetch() { - return _patchFetch({ staticGenerationAsyncStorage }) + return _patchFetch({ staticGenerationAsyncStorage, requestAsyncStorage }) } export { @@ -46,6 +44,5 @@ export { requestAsyncStorage, staticGenerationAsyncStorage, serverHooks, - originalPathname, patchFetch, } diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 8c1e9d3305fd7..666f84e71572a 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1384,9 +1384,8 @@ export async function buildAppStaticPaths({ return StaticGenerationAsyncStorageWrapper.wrap( ComponentMod.staticGenerationAsyncStorage, { - urlPathname: page, + page, renderOpts: { - originalPathname: page, incrementalCache, supportsDynamicResponse: true, isRevalidate: false, diff --git a/packages/next/src/build/webpack/loaders/next-app-loader.ts b/packages/next/src/build/webpack/loaders/next-app-loader.ts index 5d49a3a3e3fad..f90b349b3ca17 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader.ts @@ -144,7 +144,6 @@ async function createAppRouteCode({ VAR_DEFINITION_FILENAME: fileBaseName, VAR_DEFINITION_BUNDLE_PATH: bundlePath, VAR_RESOLVED_PAGE_PATH: resolvedPagePath, - VAR_ORIGINAL_PATHNAME: page, }, { nextConfigOutput: JSON.stringify(nextConfigOutput), @@ -772,7 +771,6 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { VAR_DEFINITION_PAGE: page, VAR_DEFINITION_PATHNAME: pathname, VAR_MODULE_GLOBAL_ERROR: treeCodeResult.globalError, - VAR_ORIGINAL_PATHNAME: page, }, { tree: treeCodeResult.treeCode, diff --git a/packages/next/src/client/components/request-async-storage.external.ts b/packages/next/src/client/components/request-async-storage.external.ts index e2c696bed2319..c15dfac24491e 100644 --- a/packages/next/src/client/components/request-async-storage.external.ts +++ b/packages/next/src/client/components/request-async-storage.external.ts @@ -10,6 +10,23 @@ import type { DeepReadonly } from '../../shared/lib/deep-readonly' import type { AfterContext } from '../../server/after/after-context' export interface RequestStore { + /** + * The URL of the request. This only specifies the pathname and the search + * part of the URL. + */ + readonly url: { + /** + * The pathname of the requested URL. + */ + readonly pathname: string + + /** + * The search part of the requested URL. If the request did not provide a + * search part, this will be an empty string. + */ + readonly search: string + } + readonly headers: ReadonlyHeaders readonly cookies: ReadonlyRequestCookies readonly mutableCookies: ResponseCookies diff --git a/packages/next/src/client/components/static-generation-async-storage.external.ts b/packages/next/src/client/components/static-generation-async-storage.external.ts index fc6ac8a4ad3d6..3a378e0cac243 100644 --- a/packages/next/src/client/components/static-generation-async-storage.external.ts +++ b/packages/next/src/client/components/static-generation-async-storage.external.ts @@ -10,8 +10,11 @@ import { staticGenerationAsyncStorage } from './static-generation-async-storage- export interface StaticGenerationStore { readonly isStaticGeneration: boolean - readonly pagePath?: string - readonly urlPathname: string + /** + * The page that is being rendered. This is the path to the page file. + */ + readonly page: string + readonly incrementalCache?: IncrementalCache readonly isOnDemandRevalidate?: boolean readonly isPrerendering?: boolean diff --git a/packages/next/src/export/routes/app-route.ts b/packages/next/src/export/routes/app-route.ts index 66db13b36347a..6ac3155a69db9 100644 --- a/packages/next/src/export/routes/app-route.ts +++ b/packages/next/src/export/routes/app-route.ts @@ -69,8 +69,7 @@ export async function exportAppRoute( notFoundRoutes: [], }, renderOpts: { - experimental: experimental, - originalPathname: page, + experimental, nextExport: true, supportsDynamicResponse: false, incrementalCache, diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index c727420307b51..ce567ccb7a14a 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -269,7 +269,6 @@ async function exportPageImpl( fontManifest: optimizeFonts ? requireFontManifest(distDir) : undefined, locale, supportsDynamicResponse: false, - originalPathname: page, experimental: { ...input.renderOpts.experimental, isRoutePPREnabled, diff --git a/packages/next/src/lib/metadata/metadata.tsx b/packages/next/src/lib/metadata/metadata.tsx index 3808f9843e23d..6cdf5eb8c63c0 100644 --- a/packages/next/src/lib/metadata/metadata.tsx +++ b/packages/next/src/lib/metadata/metadata.tsx @@ -35,11 +35,11 @@ import { isNotFoundError } from '../../client/components/not-found' import type { MetadataContext } from './types/resolvers' export function createMetadataContext( - urlPathname: string, + pathname: string, renderOpts: AppRenderContext['renderOpts'] ): MetadataContext { return { - pathname: urlPathname.split('?')[0], + pathname, trailingSlash: renderOpts.trailingSlash, isStandaloneMode: renderOpts.nextConfigOutput === 'standalone', } diff --git a/packages/next/src/lib/url.ts b/packages/next/src/lib/url.ts index 7daf240b00f49..f5969c1019449 100644 --- a/packages/next/src/lib/url.ts +++ b/packages/next/src/lib/url.ts @@ -1,14 +1,6 @@ import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers' -export const DUMMY_ORIGIN = 'http://n' - -function getUrlWithoutHost(url: string) { - return new URL(url, DUMMY_ORIGIN) -} - -export function getPathname(url: string) { - return getUrlWithoutHost(url).pathname -} +const DUMMY_ORIGIN = 'http://n' export function isFullStringUrl(url: string) { return /https?:\/\//.test(url) diff --git a/packages/next/src/server/after/after-context.ts b/packages/next/src/server/after/after-context.ts index 874390a07097e..2d75d280ce2c6 100644 --- a/packages/next/src/server/after/after-context.ts +++ b/packages/next/src/server/after/after-context.ts @@ -143,6 +143,7 @@ function wrapRequestStoreForAfterCallbacks( requestStore: RequestStore ): RequestStore { return { + url: requestStore.url, get headers() { return requestStore.headers }, diff --git a/packages/next/src/server/after/after.ts b/packages/next/src/server/after/after.ts index d9863a9048ad2..abc37edb8a2f9 100644 --- a/packages/next/src/server/after/after.ts +++ b/packages/next/src/server/after/after.ts @@ -1,7 +1,6 @@ import { getExpectedRequestStore } from '../../client/components/request-async-storage.external' import { staticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage.external' import { StaticGenBailoutError } from '../../client/components/static-generation-bailout' -import { getPathname } from '../../lib/url' import { markCurrentScopeAsDynamic } from '../app-render/dynamic-rendering' @@ -27,9 +26,8 @@ export function unstable_after(task: AfterTask) { if (staticGenerationStore) { if (staticGenerationStore.forceStatic) { - const pathname = getPathname(staticGenerationStore.urlPathname) throw new StaticGenBailoutError( - `Route ${pathname} with \`dynamic = "force-static"\` couldn't be rendered statically because it used \`${callingExpression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` + `Route ${staticGenerationStore.page} with \`dynamic = "force-static"\` couldn't be rendered statically because it used \`${callingExpression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) } else { markCurrentScopeAsDynamic(staticGenerationStore, callingExpression) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index f8b94d47fb9c4..a10b2d50e25bf 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -70,7 +70,6 @@ import { import { getSegmentParam } from './get-segment-param' import { getScriptNonceFromHeader } from './get-script-nonce-from-header' import { parseAndValidateFlightRouterState } from './parse-and-validate-flight-router-state' -import { validateURL } from './validate-url' import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree' import { handleAction } from './action-handler' import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' @@ -116,6 +115,7 @@ import { import { createServerModuleMap } from './action-utils' import { isNodeNextRequest } from '../base-http/helpers' import { parseParameter } from '../../shared/lib/router/utils/route-regex' +import { parseRelativeUrl } from '../../shared/lib/router/utils/parse-relative-url' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -319,7 +319,7 @@ async function generateFlight( }, getDynamicParamFromSegment, appUsingSizeAdjustment, - staticGenerationStore: { urlPathname }, + requestStore: { url }, query, requestId, flightRouterState, @@ -329,7 +329,7 @@ async function generateFlight( const [MetadataTree, MetadataOutlet] = createMetadataComponents({ tree: loaderTree, query, - metadataContext: createMetadataContext(urlPathname, ctx.renderOpts), + metadataContext: createMetadataContext(url.pathname, ctx.renderOpts), getDynamicParamFromSegment, appUsingSizeAdjustment, createDynamicallyTrackedSearchParams, @@ -441,7 +441,7 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { GlobalError, createDynamicallyTrackedSearchParams, }, - staticGenerationStore: { urlPathname }, + requestStore: { url }, } = ctx const initialTree = createFlightRouterStateFromLoaderTree( tree, @@ -453,7 +453,7 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { tree, errorType: asNotFound ? 'not-found' : undefined, query, - metadataContext: createMetadataContext(urlPathname, ctx.renderOpts), + metadataContext: createMetadataContext(url.pathname, ctx.renderOpts), getDynamicParamFromSegment: getDynamicParamFromSegment, appUsingSizeAdjustment: appUsingSizeAdjustment, createDynamicallyTrackedSearchParams, @@ -485,7 +485,7 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { { - // TODO: this includes query string, should it? - const pathname = validateURL(req.url) + if (!req.url) { + throw new Error('Invalid URL') + } + + const url = parseRelativeUrl(req.url, undefined, false) return RequestAsyncStorageWrapper.wrap( renderOpts.ComponentMod.requestAsyncStorage, - { req, res, renderOpts }, + { req, url, res, renderOpts }, (requestStore) => StaticGenerationAsyncStorageWrapper.wrap( renderOpts.ComponentMod.staticGenerationAsyncStorage, { - urlPathname: pathname, + page: renderOpts.routeModule.definition.page, renderOpts, requestEndedState: { ended: false }, }, diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index acee5b460119a..d2c31ea123199 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -227,10 +227,7 @@ async function createComponentTreeInternal({ } if (typeof layoutOrPageMod?.revalidate !== 'undefined') { - validateRevalidate( - layoutOrPageMod?.revalidate, - staticGenerationStore.urlPathname - ) + validateRevalidate(layoutOrPageMod?.revalidate, staticGenerationStore.page) } if (typeof layoutOrPageMod?.revalidate === 'number') { @@ -537,7 +534,7 @@ async function createComponentTreeInternal({ , loadingData, ], diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 24b0fb2473edc..dc3561d355164 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -26,7 +26,6 @@ import React from 'react' import type { StaticGenerationStore } from '../../client/components/static-generation-async-storage.external' import { DynamicServerError } from '../../client/components/hooks-server-context' import { StaticGenBailoutError } from '../../client/components/static-generation-bailout' -import { getPathname } from '../../lib/url' const hasPostpone = typeof React.unstable_postpone === 'function' @@ -86,11 +85,9 @@ export function markCurrentScopeAsDynamic( // or it's static and it should not throw or postpone here. if (store.forceDynamic || store.forceStatic) return - const pathname = getPathname(store.urlPathname) - if (store.dynamicShouldError) { throw new StaticGenBailoutError( - `Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` + `Route ${store.page} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) } @@ -101,7 +98,7 @@ export function markCurrentScopeAsDynamic( // We track that we had a dynamic scope that postponed. // This will be used by the renderer to decide whether // the prerender requires a resume - postponeWithTracking(store.prerenderState, expression, pathname) + postponeWithTracking(store.prerenderState, expression, store.page) } store.revalidate = 0 @@ -109,7 +106,7 @@ export function markCurrentScopeAsDynamic( if (store.isStaticGeneration) { // We aren't prerendering but we are generating a static page. We need to bail out of static generation const err = new DynamicServerError( - `Route ${pathname} couldn't be rendered statically because it used ${expression}. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` + `Route ${store.page} couldn't be rendered statically because it used ${expression}. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` ) store.dynamicUsageDescription = expression store.dynamicUsageStack = err.stack @@ -131,14 +128,13 @@ export function trackDynamicDataAccessed( store: StaticGenerationStore, expression: string ): void { - const pathname = getPathname(store.urlPathname) if (store.isUnstableCacheCallback) { throw new Error( - `Route ${pathname} used "${expression}" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` + `Route ${store.page} used "${expression}" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) } else if (store.dynamicShouldError) { throw new StaticGenBailoutError( - `Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` + `Route ${store.page} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) } else if ( // We are in a prerender (PPR enabled, during build) @@ -147,14 +143,14 @@ export function trackDynamicDataAccessed( // We track that we had a dynamic scope that postponed. // This will be used by the renderer to decide whether // the prerender requires a resume - postponeWithTracking(store.prerenderState, expression, pathname) + postponeWithTracking(store.prerenderState, expression, store.page) } else { store.revalidate = 0 if (store.isStaticGeneration) { // We aren't prerendering but we are generating a static page. We need to bail out of static generation const err = new DynamicServerError( - `Route ${pathname} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` + `Route ${store.page} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` ) store.dynamicUsageDescription = expression store.dynamicUsageStack = err.stack @@ -170,24 +166,24 @@ export function trackDynamicDataAccessed( type PostponeProps = { reason: string prerenderState: PrerenderState - pathname: string + page: string } export function Postpone({ reason, prerenderState, - pathname, + page, }: PostponeProps): never { - postponeWithTracking(prerenderState, reason, pathname) + postponeWithTracking(prerenderState, reason, page) } function postponeWithTracking( prerenderState: PrerenderState, expression: string, - pathname: string + page: string ): never { assertPostpone() const reason = - `Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` + + `Route ${page} needs to bail out of prerendering at this point because it used ${expression}. ` + `React throws this special object to indicate where. It should not be caught by ` + `your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error` diff --git a/packages/next/src/server/app-render/entry-base.ts b/packages/next/src/server/app-render/entry-base.ts index 9a5ec61699cfe..e7160c2a7f221 100644 --- a/packages/next/src/server/app-render/entry-base.ts +++ b/packages/next/src/server/app-render/entry-base.ts @@ -44,7 +44,7 @@ function patchCacheScopeSupportIntoReact() { // patchFetch makes use of APIs such as `React.unstable_postpone` which are only available // in the experimental channel of React, so export it from here so that it comes from the bundled runtime function patchFetch() { - return _patchFetch({ staticGenerationAsyncStorage }) + return _patchFetch({ staticGenerationAsyncStorage, requestAsyncStorage }) } export { diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 47ea6ff38ccf7..d9fded143cd13 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -143,7 +143,6 @@ export interface RenderOptsPartial { nextExport?: boolean nextConfigOutput?: 'standalone' | 'export' appDirDevErrorLogger?: (err: any) => Promise - originalPathname?: string isDraftMode?: boolean deploymentId?: string onUpdateCookies?: (cookies: string[]) => void diff --git a/packages/next/src/server/app-render/validate-url.tsx b/packages/next/src/server/app-render/validate-url.tsx deleted file mode 100644 index f43b1061b4398..0000000000000 --- a/packages/next/src/server/app-render/validate-url.tsx +++ /dev/null @@ -1,18 +0,0 @@ -const DUMMY_ORIGIN = 'http://n' -const INVALID_URL_MESSAGE = 'Invalid request URL' - -export function validateURL(url: string | undefined): string { - if (!url) { - throw new Error(INVALID_URL_MESSAGE) - } - try { - const parsed = new URL(url, DUMMY_ORIGIN) - // Avoid origin change by extra slashes in pathname - if (parsed.origin !== DUMMY_ORIGIN) { - throw new Error(INVALID_URL_MESSAGE) - } - return url - } catch { - throw new Error(INVALID_URL_MESSAGE) - } -} diff --git a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts index f597987da07d5..1740a16c21712 100644 --- a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts @@ -40,19 +40,39 @@ function getMutableCookies( return MutableRequestCookiesAdapter.wrap(cookies, onUpdateCookies) } -export type WrapperRenderOpts = Omit & - RequestLifecycleOpts & +export type WrapperRenderOpts = RequestLifecycleOpts & Partial< Pick< RenderOpts, - 'ComponentMod' // can be undefined in a route handler + | 'ComponentMod' + | 'onUpdateCookies' + | 'assetPrefix' + | 'reactLoadableManifest' > > & { experimental: Pick + previewProps?: __ApiPreviewProps } export type RequestContext = { req: IncomingMessage | BaseNextRequest | NextRequest + /** + * The URL of the request. This only specifies the pathname and the search + * part of the URL. This is only undefined when generating static paths (ie, + * there is no request in progress, nor do we know one). + */ + url: { + /** + * The pathname of the requested URL. + */ + pathname: string + + /** + * The search part of the requested URL. If the request did not provide a + * search part, this will be an empty string. + */ + search?: string + } res?: ServerResponse | BaseNextResponse renderOpts?: WrapperRenderOpts } @@ -72,16 +92,9 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper< */ wrap( storage: AsyncLocalStorage, - { req, res, renderOpts }: RequestContext, + { req, url, res, renderOpts }: RequestContext, callback: (store: RequestStore) => Result ): Result { - let previewProps: __ApiPreviewProps | undefined = undefined - - if (renderOpts && 'previewProps' in renderOpts) { - // TODO: investigate why previewProps isn't on RenderOpts - previewProps = (renderOpts as any).previewProps - } - const [wrapWithAfter, afterContext] = createAfterWrapper(renderOpts) function defaultOnUpdateCookies(cookies: string[]) { @@ -98,6 +111,10 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper< } = {} const store: RequestStore = { + // Rather than just using the whole `url` here, we pull the parts we want + // to ensure we don't use parts of the URL that we shouldn't. This also + // lets us avoid requiring an empty string for `search` in the type. + url: { pathname: url.pathname, search: url.search ?? '' }, get headers() { if (!cache.headers) { // Seal the headers object that'll freeze out any methods that could @@ -154,7 +171,7 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper< get draftMode() { if (!cache.draftMode) { cache.draftMode = new DraftModeProvider( - previewProps, + renderOpts?.previewProps, req, this.cookies, this.mutableCookies diff --git a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts index 956cf140437f2..eb4f9d4ae4e83 100644 --- a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts @@ -4,12 +4,16 @@ import type { AsyncLocalStorage } from 'async_hooks' import type { IncrementalCache } from '../lib/incremental-cache' import type { RenderOptsPartial } from '../app-render/types' -import { createPrerenderState } from '../../server/app-render/dynamic-rendering' +import { createPrerenderState } from '../app-render/dynamic-rendering' import type { FetchMetric } from '../base-http' import type { RequestLifecycleOpts } from '../base-server' export type StaticGenerationContext = { - urlPathname: string + /** + * The page that is being rendered. This relates to the path to the page file. + */ + page: string + requestEndedState?: { ended?: boolean } renderOpts: { incrementalCache?: IncrementalCache @@ -40,7 +44,6 @@ export type StaticGenerationContext = { // Pull some properties from RenderOptsPartial so that the docs are also // mirrored. RenderOptsPartial, - | 'originalPathname' | 'supportsDynamicResponse' | 'isRevalidate' | 'nextExport' @@ -56,7 +59,7 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper< > = { wrap( storage: AsyncLocalStorage, - { urlPathname, renderOpts, requestEndedState }: StaticGenerationContext, + { page, renderOpts, requestEndedState }: StaticGenerationContext, callback: (store: StaticGenerationStore) => Result ): Result { /** @@ -88,8 +91,7 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper< const store: StaticGenerationStore = { isStaticGeneration, - urlPathname, - pagePath: renderOpts.originalPathname, + page, incrementalCache: // we fallback to a global incremental cache for edge-runtime locally // so that it can access the fs cache without mocks diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index d5ca7ebf50195..f948f311ce8e6 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2340,7 +2340,6 @@ export default abstract class Server< // it is not a dynamic RSC request then it is a revalidation // request. isRevalidate: isSSG && !postponed && !isDynamicRSCRequest, - originalPathname: components.ComponentMod.originalPathname, serverActions: this.nextConfig.experimental.serverActions, } : {}), @@ -2409,7 +2408,6 @@ export default abstract class Server< experimental: { after: renderOpts.experimental.after, }, - originalPathname: components.ComponentMod.originalPathname, supportsDynamicResponse, incrementalCache, isRevalidate: isSSG, diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index fe312aa0f62b1..d3b1a3540cc8a 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -15,6 +15,10 @@ import * as Log from '../../build/output/log' import { markCurrentScopeAsDynamic } from '../app-render/dynamic-rendering' import type { FetchMetric } from '../base-http' import { createDedupeFetch } from './dedupe-fetch' +import type { + RequestAsyncStorage, + RequestStore, +} from '../../client/components/request-async-storage.external' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -34,7 +38,7 @@ function isPatchedFetch( export function validateRevalidate( revalidateVal: unknown, - pathname: string + page: string ): undefined | number | false { try { let normalizedRevalidate: false | number | undefined = undefined @@ -49,7 +53,7 @@ export function validateRevalidate( normalizedRevalidate = revalidateVal } else if (typeof revalidateVal !== 'undefined') { throw new Error( - `Invalid revalidate value "${revalidateVal}" on "${pathname}", must be a non-negative number or "false"` + `Invalid revalidate value "${revalidateVal}" on "${page}", must be a non-negative number or "false"` ) } return normalizedRevalidate @@ -127,35 +131,35 @@ const getDerivedTags = (pathname: string): string[] => { return derivedTags } -export function addImplicitTags(staticGenerationStore: StaticGenerationStore) { +export function addImplicitTags( + staticGenerationStore: StaticGenerationStore, + requestStore: RequestStore | undefined +) { const newTags: string[] = [] - const { pagePath, urlPathname } = staticGenerationStore + const { page } = staticGenerationStore - if (!Array.isArray(staticGenerationStore.tags)) { - staticGenerationStore.tags = [] - } - - if (pagePath) { - const derivedTags = getDerivedTags(pagePath) + // Ini the tags array if it doesn't exist. + staticGenerationStore.tags ??= [] - for (let tag of derivedTags) { - tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}` - if (!staticGenerationStore.tags?.includes(tag)) { - staticGenerationStore.tags.push(tag) - } - newTags.push(tag) + // Add the derived tags from the page. + const derivedTags = getDerivedTags(page) + for (let tag of derivedTags) { + tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}` + if (!staticGenerationStore.tags?.includes(tag)) { + staticGenerationStore.tags.push(tag) } + newTags.push(tag) } - if (urlPathname) { - const parsedPathname = new URL(urlPathname, 'http://n').pathname - - const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${parsedPathname}` + // Add the tags from the pathname. + if (requestStore?.url.pathname) { + const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${requestStore.url.pathname}` if (!staticGenerationStore.tags?.includes(tag)) { staticGenerationStore.tags.push(tag) } newTags.push(tag) } + return newTags } @@ -210,11 +214,12 @@ function trackFetchMetric( interface PatchableModule { staticGenerationAsyncStorage: StaticGenerationAsyncStorage + requestAsyncStorage: RequestAsyncStorage } function createPatchedFetcher( originFetch: Fetcher, - { staticGenerationAsyncStorage }: PatchableModule + { staticGenerationAsyncStorage, requestAsyncStorage }: PatchableModule ): PatchedFetcher { // Create the patched fetch function. We don't set the type here, as it's // verified as the return value of this function. @@ -260,6 +265,7 @@ function createPatchedFetcher( } const staticGenerationStore = staticGenerationAsyncStorage.getStore() + const requestStore = requestAsyncStorage.getStore() // If the staticGenerationStore is not available, we can't do any // special treatment of fetch, therefore fallback to the original @@ -311,7 +317,10 @@ function createPatchedFetcher( } } } - const implicitTags = addImplicitTags(staticGenerationStore) + const implicitTags = addImplicitTags( + staticGenerationStore, + requestStore + ) const pageFetchCacheMode = staticGenerationStore.fetchCache const isUsingNoStore = !!staticGenerationStore.isUnstableNoStore @@ -327,7 +336,7 @@ function createPatchedFetcher( // we only want to warn if the user is explicitly setting a cache value if (!(isRequestInput && currentFetchCacheConfig === 'default')) { Log.warn( - `fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.` + `fetch for ${fetchUrl} on ${staticGenerationStore.page} specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.` ) } currentFetchCacheConfig = undefined @@ -359,7 +368,7 @@ function createPatchedFetcher( finalRevalidate = validateRevalidate( currentFetchRevalidate, - staticGenerationStore.urlPathname + staticGenerationStore.page ) const _headers = getRequestMeta('headers') @@ -484,11 +493,7 @@ function createPatchedFetcher( if (finalRevalidate === 0) { markCurrentScopeAsDynamic( staticGenerationStore, - `revalidate: 0 fetch ${input}${ - staticGenerationStore.urlPathname - ? ` ${staticGenerationStore.urlPathname}` - : '' - }` + `revalidate: 0 fetch ${input} ${staticGenerationStore.page}` ) } @@ -711,11 +716,7 @@ function createPatchedFetcher( // If enabled, we should bail out of static generation. markCurrentScopeAsDynamic( staticGenerationStore, - `no-store fetch ${input}${ - staticGenerationStore.urlPathname - ? ` ${staticGenerationStore.urlPathname}` - : '' - }` + `no-store fetch ${input} ${staticGenerationStore.page}` ) } @@ -731,11 +732,7 @@ function createPatchedFetcher( // If enabled, we should bail out of static generation. markCurrentScopeAsDynamic( staticGenerationStore, - `revalidate: 0 fetch ${input}${ - staticGenerationStore.urlPathname - ? ` ${staticGenerationStore.urlPathname}` - : '' - }` + `revalidate: 0 fetch ${input} ${staticGenerationStore.page}` ) } diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 0d01343a5d7e8..5e9fc519c2fc6 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -64,7 +64,7 @@ export type LoadComponentsReturnType = { getStaticPaths?: GetStaticPaths getServerSideProps?: GetServerSideProps ComponentMod: NextModule - routeModule?: RouteModule + routeModule: RouteModule isAppPath?: boolean page: string } diff --git a/packages/next/src/server/load-default-error-components.ts b/packages/next/src/server/load-default-error-components.ts index b1b0f669d3791..8e35f2469404b 100644 --- a/packages/next/src/server/load-default-error-components.ts +++ b/packages/next/src/server/load-default-error-components.ts @@ -40,7 +40,7 @@ export type LoadComponentsReturnType = { getStaticPaths?: GetStaticPaths getServerSideProps?: GetServerSideProps ComponentMod: any - routeModule?: RouteModule + routeModule: RouteModule isAppPath?: boolean page: string } diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 63e76265104bf..0b76b07a7eae1 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -257,19 +257,18 @@ export class AppRouteRouteModule extends RouteModule< // Get the context for the request. const requestContext: RequestContext = { req: rawRequest, - } - - requestContext.renderOpts = { - // @ts-expect-error TODO: types for renderOpts should include previewProps - previewProps: context.prerenderManifest.preview, - waitUntil: context.renderOpts.waitUntil, - onClose: context.renderOpts.onClose, - experimental: context.renderOpts.experimental, + url: rawRequest.nextUrl, + renderOpts: { + previewProps: context.prerenderManifest.preview, + waitUntil: context.renderOpts.waitUntil, + onClose: context.renderOpts.onClose, + experimental: context.renderOpts.experimental, + }, } // Get the context for the static generation. const staticGenerationContext: StaticGenerationContext = { - urlPathname: rawRequest.nextUrl.pathname, + page: this.definition.page, renderOpts: context.renderOpts, } @@ -288,7 +287,7 @@ export class AppRouteRouteModule extends RouteModule< RequestAsyncStorageWrapper.wrap( this.requestAsyncStorage, requestContext, - () => + (requestStore) => StaticGenerationAsyncStorageWrapper.wrap( this.staticGenerationAsyncStorage, staticGenerationContext, @@ -375,6 +374,7 @@ export class AppRouteRouteModule extends RouteModule< patchFetch({ staticGenerationAsyncStorage: this.staticGenerationAsyncStorage, + requestAsyncStorage: this.requestAsyncStorage, }) const res = await handler(request, { params: context.params @@ -398,14 +398,13 @@ export class AppRouteRouteModule extends RouteModule< ), ]) - addImplicitTags(staticGenerationStore) + addImplicitTags(staticGenerationStore, requestStore) ;(context.renderOpts as any).fetchTags = staticGenerationStore.tags?.join(',') // It's possible cookies were set in the handler, so we need // to merge the modified cookies and the returned response // here. - const requestStore = this.requestAsyncStorage.getStore() if (requestStore && requestStore.mutableCookies) { const headers = new Headers(res.headers) if ( diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 42100a08b78ff..701c0c1a27217 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -240,20 +240,22 @@ export async function adapter( }, async () => { try { + const previewProps = prerenderManifest?.preview || { + previewModeId: 'development-id', + previewModeEncryptionKey: '', + previewModeSigningKey: '', + } + return await RequestAsyncStorageWrapper.wrap( requestAsyncStorage, { req: request, + url: request.nextUrl, renderOpts: { onUpdateCookies: (cookies) => { cookiesFromResponse = cookies }, - // @ts-expect-error TODO: investigate why previewProps isn't on RenderOpts - previewProps: prerenderManifest?.preview || { - previewModeId: 'development-id', - previewModeEncryptionKey: '', - previewModeSigningKey: '', - }, + previewProps, waitUntil, onClose: closeController ? closeController.onClose.bind(closeController) diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index 00996c7d188f2..01acc3a07ac34 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -4,7 +4,6 @@ import { NEXT_CACHE_IMPLICIT_TAG_ID, NEXT_CACHE_SOFT_TAG_MAX_LENGTH, } from '../../../lib/constants' -import { getPathname } from '../../../lib/url' import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external' /** @@ -51,9 +50,7 @@ function revalidate(tag: string, expression: string) { if (store.isUnstableCacheCallback) { throw new Error( - `Route ${getPathname( - store.urlPathname - )} used "${expression}" inside a function cached with "unstable_cache(...)" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` + `Route ${store.page} used "${expression}" inside a function cached with "unstable_cache(...)" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) } diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index e132279e0a94e..b23ad05e38062 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -7,6 +7,7 @@ import { validateTags, } from '../../lib/patch-fetch' import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external' +import { requestAsyncStorage } from '../../../client/components/request-async-storage.external' type Callback = (...args: any[]) => Promise @@ -90,13 +91,15 @@ export function unstable_cache( }` const cachedCb = async (...args: any[]) => { - const store = staticGenerationAsyncStorage.getStore() + const staticGenerationStore = staticGenerationAsyncStorage.getStore() + const requestStore = requestAsyncStorage.getStore() // We must be able to find the incremental cache otherwise we throw const maybeIncrementalCache: | import('../../lib/incremental-cache').IncrementalCache | undefined = - store?.incrementalCache || (globalThis as any).__incrementalCache + staticGenerationStore?.incrementalCache || + (globalThis as any).__incrementalCache if (!maybeIncrementalCache) { throw new Error( @@ -105,10 +108,14 @@ export function unstable_cache( } const incrementalCache = maybeIncrementalCache - const { pathname, searchParams } = new URL( - store?.urlPathname || '/', - 'http://n' - ) + // If there's no request store, we aren't in a request (or we're not in app + // router) and if there's no static generation store, we aren't in app + // router. Default to an empty pathname and search params when there's no + // request store or static generation store available. + const pathname = + requestStore?.url.pathname ?? staticGenerationStore?.page ?? '' + const searchParams = new URLSearchParams(requestStore?.url.search ?? '') + const sortedSearchKeys = [...searchParams.keys()].sort((a, b) => { return a.localeCompare(b) }) @@ -123,10 +130,13 @@ export function unstable_cache( const cacheKey = await incrementalCache.fetchCacheKey(invocationKey) // $urlWithPath,$sortedQueryStringKeys,$hashOfEveryThingElse const fetchUrl = `unstable_cache ${pathname}${sortedSearch.length ? '?' : ''}${sortedSearch} ${cb.name ? ` ${cb.name}` : cacheKey}` - const fetchIdx = (store ? store.nextFetchId : noStoreFetchIdx) ?? 1 + const fetchIdx = + (staticGenerationStore + ? staticGenerationStore.nextFetchId + : noStoreFetchIdx) ?? 1 - if (store) { - store.nextFetchId = fetchIdx + 1 + if (staticGenerationStore) { + staticGenerationStore.nextFetchId = fetchIdx + 1 // We are in an App Router context. We try to return the cached entry if it exists and is valid // If the entry is fresh we return it. If the entry is stale we return it but revalidate the entry in @@ -135,43 +145,43 @@ export function unstable_cache( // We update the store's revalidate property if the option.revalidate is a higher precedence if (typeof options.revalidate === 'number') { if ( - typeof store.revalidate === 'number' && - store.revalidate < options.revalidate + typeof staticGenerationStore.revalidate === 'number' && + staticGenerationStore.revalidate < options.revalidate ) { // The store is already revalidating on a shorter time interval, leave it alone } else { - store.revalidate = options.revalidate + staticGenerationStore.revalidate = options.revalidate } } else if ( options.revalidate === false && - typeof store.revalidate === 'undefined' + typeof staticGenerationStore.revalidate === 'undefined' ) { // The store has not defined revalidate type so we can use the false option - store.revalidate = options.revalidate + staticGenerationStore.revalidate = options.revalidate } // We need to accumulate the tags for this invocation within the store - if (!store.tags) { - store.tags = tags.slice() + if (!staticGenerationStore.tags) { + staticGenerationStore.tags = tags.slice() } else { for (const tag of tags) { // @TODO refactor tags to be a set to avoid this O(n) lookup - if (!store.tags.includes(tag)) { - store.tags.push(tag) + if (!staticGenerationStore.tags.includes(tag)) { + staticGenerationStore.tags.push(tag) } } } // @TODO check on this API. addImplicitTags mutates the store and returns the implicit tags. The naming // of this function is potentially a little confusing - const implicitTags = addImplicitTags(store) + const implicitTags = addImplicitTags(staticGenerationStore, requestStore) if ( // when we are nested inside of other unstable_cache's // we should bypass cache similar to fetches - store.fetchCache !== 'force-no-store' && - !store.isOnDemandRevalidate && + staticGenerationStore.fetchCache !== 'force-no-store' && + !staticGenerationStore.isOnDemandRevalidate && !incrementalCache.isOnDemandRevalidate && - !store.isDraftMode + !staticGenerationStore.isDraftMode ) { // We attempt to get the current cache entry from the incremental cache. const cacheEntry = await incrementalCache.get(cacheKey, { @@ -203,15 +213,15 @@ export function unstable_cache( : undefined if (cacheEntry.isStale) { // In App Router we return the stale result and revalidate in the background - if (!store.pendingRevalidates) { - store.pendingRevalidates = {} + if (!staticGenerationStore.pendingRevalidates) { + staticGenerationStore.pendingRevalidates = {} } // We run the cache function asynchronously and save the result when it completes - store.pendingRevalidates[invocationKey] = + staticGenerationStore.pendingRevalidates[invocationKey] = staticGenerationAsyncStorage .run( { - ...store, + ...staticGenerationStore, // force any nested fetches to bypass cache so they revalidate // when the unstable_cache call is revalidated fetchCache: 'force-no-store', @@ -248,7 +258,7 @@ export function unstable_cache( // If we got this far then we had an invalid cache entry and need to generate a new one const result = await staticGenerationAsyncStorage.run( { - ...store, + ...staticGenerationStore, // force any nested fetches to bypass cache so they revalidate // when the unstable_cache call is revalidated fetchCache: 'force-no-store', @@ -279,7 +289,9 @@ export function unstable_cache( // @TODO check on this API. addImplicitTags mutates the store and returns the implicit tags. The naming // of this function is potentially a little confusing - const implicitTags = store && addImplicitTags(store) + const implicitTags = + staticGenerationStore && + addImplicitTags(staticGenerationStore, requestStore) const cacheEntry = await incrementalCache.get(cacheKey, { kindHint: 'fetch', @@ -325,7 +337,7 @@ export function unstable_cache( // when the unstable_cache call is revalidated fetchCache: 'force-no-store', isUnstableCacheCallback: true, - urlPathname: '/', + page: '/', isStaticGeneration: false, prerenderState: null, }, diff --git a/packages/next/src/shared/lib/router/utils/parse-relative-url.test.ts b/packages/next/src/shared/lib/router/utils/parse-relative-url.test.ts new file mode 100644 index 0000000000000..8c71acfda0b83 --- /dev/null +++ b/packages/next/src/shared/lib/router/utils/parse-relative-url.test.ts @@ -0,0 +1,20 @@ +import { parseRelativeUrl } from './parse-relative-url' + +describe('relative urls', () => { + it('should return valid pathname', () => { + expect(parseRelativeUrl('/').pathname).toBe('/') + expect(parseRelativeUrl('/abc').pathname).toBe('/abc') + }) + + it('should throw for invalid pathname', () => { + expect(() => parseRelativeUrl('//**y/\\')).toThrow() + expect(() => parseRelativeUrl('//google.com')).toThrow() + }) +}) + +describe('query parsing', () => { + it('should parse query string', () => { + expect(parseRelativeUrl('/?a=1&b=2').query).toEqual({ a: '1', b: '2' }) + expect(parseRelativeUrl('/').query).toEqual({}) + }) +}) diff --git a/packages/next/src/shared/lib/router/utils/parse-relative-url.ts b/packages/next/src/shared/lib/router/utils/parse-relative-url.ts index e5a49bca4d159..4abc04fa23c11 100644 --- a/packages/next/src/shared/lib/router/utils/parse-relative-url.ts +++ b/packages/next/src/shared/lib/router/utils/parse-relative-url.ts @@ -18,8 +18,19 @@ export interface ParsedRelativeUrl { */ export function parseRelativeUrl( url: string, - base?: string -): ParsedRelativeUrl { + base?: string, + parseQuery?: true +): ParsedRelativeUrl +export function parseRelativeUrl( + url: string, + base: string | undefined, + parseQuery: false +): Omit +export function parseRelativeUrl( + url: string, + base?: string, + parseQuery = true +): ParsedRelativeUrl | Omit { const globalBase = new URL( typeof window === 'undefined' ? 'http://n' : getLocationOrigin() ) @@ -36,14 +47,16 @@ export function parseRelativeUrl( url, resolvedBase ) + if (origin !== globalBase.origin) { throw new Error(`invariant: invalid relative URL, router received ${url}`) } + return { pathname, - query: searchParamsToUrlQuery(searchParams), + query: parseQuery ? searchParamsToUrlQuery(searchParams) : undefined, search, hash, - href: href.slice(globalBase.origin.length), + href: href.slice(origin.length), } } diff --git a/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts b/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts index c886a718d6004..4c1bb545147b6 100644 --- a/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts +++ b/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts @@ -5,7 +5,7 @@ import { matchHas, prepareDestination } from './prepare-destination' import { removeTrailingSlash } from './remove-trailing-slash' import { normalizeLocalePath } from '../../i18n/normalize-locale-path' import { removeBasePath } from '../../../../client/remove-base-path' -import { parseRelativeUrl } from './parse-relative-url' +import { parseRelativeUrl, type ParsedRelativeUrl } from './parse-relative-url' export default function resolveRewrites( asPath: string, @@ -20,7 +20,7 @@ export default function resolveRewrites( locales?: string[] ): { matchedPage: boolean - parsedAs: ReturnType + parsedAs: ParsedRelativeUrl asPath: string resolvedHref?: string externalDest?: boolean diff --git a/test/unit/validate-url.test.ts b/test/unit/validate-url.test.ts deleted file mode 100644 index a82e9fe985ab1..0000000000000 --- a/test/unit/validate-url.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { validateURL } from 'next/dist/server/app-render/validate-url' - -describe('validateUrl', () => { - it('should return valid pathname', () => { - expect(validateURL('/')).toBe('/') - expect(validateURL('/abc')).toBe('/abc') - }) - - it('should throw for invalid pathname', () => { - expect(() => validateURL('//**y/\\')).toThrow() - expect(() => validateURL('//google.com')).toThrow() - }) -})