diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 0ff81a5538897..fa0186ed85665 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -39,6 +39,7 @@ import type { AppRouteRouteModule, } from './future/route-modules/app-route/module' import type { Server as HTTPServer } from 'http' +import type { ImageConfigComplete } from '../shared/lib/image-config' import { format as formatUrl, parse as parseUrl } from 'url' import { formatHostname } from './lib/format-hostname' @@ -54,7 +55,6 @@ import { import { isDynamicRoute } from '../shared/lib/router/utils' import { checkIsOnDemandRevalidate } from './api-utils' import { setConfig } from '../shared/lib/runtime-config.external' - import { setRevalidateHeaders } from './send-payload/revalidate-headers' import { execOnce } from '../shared/lib/utils' import { isBlockedPage } from './utils' @@ -71,13 +71,8 @@ import { getRequestMeta, removeRequestMeta, } from './request-meta' - -import type { ImageConfigComplete } from '../shared/lib/image-config' import { removePathPrefix } from '../shared/lib/router/utils/remove-path-prefix' -import { - normalizeAppPath, - normalizeRscPath, -} from '../shared/lib/router/utils/app-paths' +import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { getHostname } from '../shared/lib/get-hostname' import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url' import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' @@ -124,6 +119,7 @@ import { import { matchNextDataPathname } from './lib/match-next-data-pathname' import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path' import { stripInternalHeaders } from './internal-utils' +import { RSCPathnameNormalizer } from './future/normalizers/request/rsc' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -140,7 +136,7 @@ export type RouteHandler = ( req: BaseNextRequest, res: BaseNextResponse, parsedUrl: NextUrlWithParsedQuery -) => PromiseLike | boolean +) => PromiseLike | boolean | void /** * The normalized route manifest is the same as the route manifest, but with @@ -387,6 +383,10 @@ export default abstract class Server { protected readonly i18nProvider?: I18NProvider protected readonly localeNormalizer?: LocaleRouteNormalizer + protected readonly normalizers: { + readonly rsc: RSCPathnameNormalizer + } + public constructor(options: ServerOptions) { const { dir = '.', @@ -449,6 +449,11 @@ export default abstract class Server { minimalMode || !!process.env.NEXT_PRIVATE_MINIMAL_MODE this.hasAppDir = this.getHasAppDir(dev) + + this.normalizers = { + rsc: new RSCPathnameNormalizer(this.hasAppDir), + } + const serverComponents = this.hasAppDir this.nextFontManifest = this.getNextFontManifest() @@ -527,11 +532,29 @@ export default abstract class Server { return this.matchers.reload() } - protected handleNextDataRequest: RouteHandler = async ( - req, - res, - parsedUrl - ) => { + private handleRSCRequest: RouteHandler = (req, _res, parsedUrl) => { + if ( + !parsedUrl.pathname || + !this.normalizers.rsc.match(parsedUrl.pathname) + ) { + return + } + + parsedUrl.query.__nextDataReq = '1' + parsedUrl.pathname = this.normalizers.rsc.normalize( + parsedUrl.pathname, + true + ) + + // Update the URL. + if (req.url) { + const parsed = parseUrl(req.url) + parsed.pathname = parsedUrl.pathname + req.url = formatUrl(parsed) + } + } + + private handleNextDataRequest: RouteHandler = async (req, res, parsedUrl) => { const middleware = this.getMiddleware() const params = matchNextDataPathname(parsedUrl.pathname) @@ -624,17 +647,9 @@ export default abstract class Server { return false } - protected handleNextImageRequest: RouteHandler = () => { - return false - } - - protected handleCatchallRenderRequest: RouteHandler = () => { - return false - } - - protected handleCatchallMiddlewareRequest: RouteHandler = () => { - return false - } + protected handleNextImageRequest: RouteHandler = () => {} + protected handleCatchallRenderRequest: RouteHandler = () => {} + protected handleCatchallMiddlewareRequest: RouteHandler = () => {} protected getRouteMatchers(): RouteMatcherManager { // Create a new manifest loader that get's the manifests from the server. @@ -801,33 +816,33 @@ export default abstract class Server { // Parse url if parsedUrl not provided if (!parsedUrl || typeof parsedUrl !== 'object') { + if (!req.url) { + throw new Error('Invariant: url can not be undefined') + } + parsedUrl = parseUrl(req.url!, true) } + if (!parsedUrl.pathname) { + throw new Error("Invariant: pathname can't be empty") + } + // Parse the querystring ourselves if the user doesn't handle querystring parsing if (typeof parsedUrl.query === 'string') { parsedUrl.query = Object.fromEntries( new URLSearchParams(parsedUrl.query) ) } - // in minimal mode we detect RSC revalidate if the .rsc - // path is requested - if (this.minimalMode) { - if (req.url.endsWith('.rsc')) { - parsedUrl.query.__nextDataReq = '1' - } else if (req.headers['x-now-route-matches']) { - for (const param of FLIGHT_PARAMETERS) { - delete req.headers[param.toString().toLowerCase()] - } + + let finished = await this.handleRSCRequest(req, res, parsedUrl) + if (finished) return + + if (this.minimalMode && req.headers['x-now-route-matches']) { + for (const param of FLIGHT_PARAMETERS) { + delete req.headers[param.toString().toLowerCase()] } } - req.url = normalizeRscPath(req.url, this.hasAppDir) - parsedUrl.pathname = normalizeRscPath( - parsedUrl.pathname || '', - this.hasAppDir - ) - this.attachRequestMeta(req, parsedUrl) const domainLocale = this.i18nProvider?.detectDomainLocale( @@ -866,12 +881,15 @@ export default abstract class Server { } // x-matched-path is the source of truth, it tells what page // should be rendered because we don't process rewrites in minimalMode - let matchedPath = normalizeRscPath( - new URL(req.headers['x-matched-path'] as string, 'http://localhost') - .pathname, - this.hasAppDir + let { pathname: matchedPath } = new URL( + req.headers['x-matched-path'] as string, + 'http://localhost' ) + if (this.normalizers.rsc.match(matchedPath)) { + matchedPath = this.normalizers.rsc.normalize(matchedPath, true) + } + let urlPathname = new URL(req.url, 'http://localhost').pathname // For ISR the URL is normalized to the prerenderPath so if @@ -1062,7 +1080,7 @@ export default abstract class Server { parsedUrl.pathname = matchedPath url.pathname = parsedUrl.pathname - const finished = await this.handleNextDataRequest(req, res, parsedUrl) + finished = await this.handle(req, res, parsedUrl) if (finished) return } catch (err) { if (err instanceof DecodeError || err instanceof NormalizeError) { @@ -1226,12 +1244,7 @@ export default abstract class Server { ) } - // Try to handle this as a `/_next/image` request. - let finished = await this.handleNextImageRequest(req, res, parsedUrl) - if (finished) return - - // Try to handle the request as a `/_next/data` request. - finished = await this.handleNextDataRequest(req, res, parsedUrl) + finished = await this.handle(req, res, parsedUrl) if (finished) return await this.handleCatchallRenderRequest(req, res, parsedUrl) @@ -1242,7 +1255,7 @@ export default abstract class Server { process.env.NEXT_RUNTIME !== 'edge' && req.headers['x-middleware-invoke'] ) { - let finished = await this.handleNextDataRequest(req, res, parsedUrl) + finished = await this.handle(req, res, parsedUrl) if (finished) return finished = await this.handleCatchallMiddlewareRequest( @@ -1264,8 +1277,11 @@ export default abstract class Server { throw err } + // This wasn't a request via the matched path or the invoke path, so + // prepare for a legacy run by removing the base path. + // ensure we strip the basePath when not using an invoke header - if (!(useMatchedPathHeader || useInvokePath) && pathnameInfo.basePath) { + if (!useMatchedPathHeader && pathnameInfo.basePath) { parsedUrl.pathname = removePathPrefix( parsedUrl.pathname, pathnameInfo.basePath @@ -1297,6 +1313,19 @@ export default abstract class Server { } } + protected handle: RouteHandler = async (req, res, url) => { + let finished = await this.handleNextImageRequest(req, res, url) + if (finished) return true + + finished = await this.handleNextDataRequest(req, res, url) + if (finished) return true + + if (this.minimalMode && this.hasAppDir) { + finished = await this.handleRSCRequest(req, res, url) + if (finished) return true + } + } + public getRequestHandler(): BaseRequestHandler { return this.handleRequest.bind(this) } diff --git a/packages/next/src/server/future/normalizers/request/base-path.test.ts b/packages/next/src/server/future/normalizers/request/base-path.test.ts new file mode 100644 index 0000000000000..48eb3858992cc --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/base-path.test.ts @@ -0,0 +1,53 @@ +import { BasePathPathnameNormalizer } from './base-path' + +describe('BasePathPathnameNormalizer', () => { + describe('match', () => { + it('should return false if there is no basePath', () => { + let normalizer = new BasePathPathnameNormalizer('') + expect(normalizer.match('/')).toBe(false) + normalizer = new BasePathPathnameNormalizer('/') + expect(normalizer.match('/')).toBe(false) + }) + + it('should return false if the pathname does not start with the basePath', () => { + const normalizer = new BasePathPathnameNormalizer('/foo') + const pathnames = ['/bar', '/bar/foo', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + + it('should return true if the pathname starts with the basePath', () => { + const normalizer = new BasePathPathnameNormalizer('/foo') + const pathnames = ['/foo', '/foo/bar', '/foo/bar/baz'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(true) + } + }) + }) + + describe('normalize', () => { + it('should return the same pathname if there is no basePath', () => { + let normalizer = new BasePathPathnameNormalizer('') + expect(normalizer.normalize('/foo')).toBe('/foo') + normalizer = new BasePathPathnameNormalizer('/') + expect(normalizer.normalize('/foo')).toBe('/foo') + }) + + it('should return the same pathname if we are not matched and the pathname does not start with the basePath', () => { + const normalizer = new BasePathPathnameNormalizer('/foo') + let pathnames = ['/bar', '/bar/foo', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname) + } + }) + + it('should strip the basePath from the pathname when it matches', () => { + const normalizer = new BasePathPathnameNormalizer('/foo') + const pathnames = ['/foo', '/foo/bar', '/foo/bar/baz'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname.substring(4)) + } + }) + }) +}) diff --git a/packages/next/src/server/future/normalizers/request/base-path.ts b/packages/next/src/server/future/normalizers/request/base-path.ts new file mode 100644 index 0000000000000..d344bca71652c --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/base-path.ts @@ -0,0 +1,32 @@ +import type { Normalizer } from '../normalizer' + +export class BasePathPathnameNormalizer implements Normalizer { + private readonly basePath?: string + constructor(basePath: string) { + // A basePath of `/` is not a basePath. + if (!basePath || basePath === '/') return + + this.basePath = basePath + } + + public match(pathname: string) { + // If there's no basePath, we don't match. + if (!this.basePath) return false + + // If the pathname doesn't start with the basePath, we don't match. + if (pathname !== this.basePath && !pathname.startsWith(this.basePath + '/')) + return false + + return true + } + + public normalize(pathname: string, matched?: boolean): string { + // If there's no basePath, we don't need to normalize. + if (!this.basePath) return pathname + + // If we're not matched and we don't match, we don't need to normalize. + if (!matched && !this.match(pathname)) return pathname + + return pathname.substring(this.basePath.length) + } +} diff --git a/packages/next/src/server/future/normalizers/request/next-data.test.ts b/packages/next/src/server/future/normalizers/request/next-data.test.ts new file mode 100644 index 0000000000000..a7a4fd985af37 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/next-data.test.ts @@ -0,0 +1,74 @@ +import { NextDataPathnameNormalizer } from './next-data' + +describe('NextDataPathnameNormalizer', () => { + describe('constructor', () => { + it('should error when no buildID is provided', () => { + expect(() => { + new NextDataPathnameNormalizer('') + }).toThrowErrorMatchingInlineSnapshot(`"Invariant: buildID is required"`) + }) + }) + + describe('match', () => { + it('should return false if the pathname does not start with the prefix', () => { + const normalizer = new NextDataPathnameNormalizer('build-id') + const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + + it('should return false if the pathname only ends with `.json`', () => { + const normalizer = new NextDataPathnameNormalizer('build-id') + const pathnames = ['/foo.json', '/foo/bar.json', '/fooo/bar.json'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + + it('should return true if it matches', () => { + const normalizer = new NextDataPathnameNormalizer('build-id') + const pathnames = [ + '/_next/data/build-id/index.json', + '/_next/data/build-id/foo.json', + '/_next/data/build-id/foo/bar.json', + '/_next/data/build-id/fooo/bar.json', + ] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(true) + } + }) + }) + + describe('normalize', () => { + it('should return the same pathname if we are not matched and the pathname does not start with the prefix', () => { + const normalizer = new NextDataPathnameNormalizer('build-id') + const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname) + } + }) + + it('should strip the prefix and the `.json` extension from the pathname when it matches', () => { + const normalizer = new NextDataPathnameNormalizer('build-id') + const pathnames = [ + '/_next/data/build-id/foo.json', + '/_next/data/build-id/foo/bar.json', + '/_next/data/build-id/fooo/bar.json', + ] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe( + pathname.substring( + '/_next/data/build-id'.length, + pathname.length - '.json'.length + ) + ) + } + }) + + it('should normalize `/index` to `/`', () => { + const normalizer = new NextDataPathnameNormalizer('build-id') + expect(normalizer.normalize('/_next/data/build-id/index.json')).toBe('/') + }) + }) +}) diff --git a/packages/next/src/server/future/normalizers/request/next-data.ts b/packages/next/src/server/future/normalizers/request/next-data.ts new file mode 100644 index 0000000000000..462a883628fc4 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/next-data.ts @@ -0,0 +1,40 @@ +import type { Normalizer } from '../normalizer' + +export class NextDataPathnameNormalizer implements Normalizer { + private readonly prefix: string + constructor(buildID: string) { + if (!buildID) { + throw new Error('Invariant: buildID is required') + } + + this.prefix = `/_next/data/${buildID}` + } + + public match(pathname: string) { + // If the pathname doesn't start with the prefix, we don't match. + if (!pathname.startsWith(`${this.prefix}/`)) return false + + // If the pathname ends with `.json`, we don't match. + if (!pathname.endsWith('.json')) return false + + return true + } + + public normalize(pathname: string, matched?: boolean): string { + // If we're not matched and we don't match, we don't need to normalize. + if (!matched && !this.match(pathname)) return pathname + + // Remove the prefix and the `.json` extension. + pathname = pathname.substring( + this.prefix.length, + pathname.length - '.json'.length + ) + + // If the pathname is `/index`, we normalize it to `/`. + if (pathname === '/index') { + return '/' + } + + return pathname + } +} diff --git a/packages/next/src/server/future/normalizers/request/rsc.test.ts b/packages/next/src/server/future/normalizers/request/rsc.test.ts new file mode 100644 index 0000000000000..5ed8c13ff9019 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/rsc.test.ts @@ -0,0 +1,58 @@ +import { RSCPathnameNormalizer } from './rsc' + +describe('RSCPathnameNormalizer', () => { + describe('match', () => { + it('should return false if the pathname does not end with `.rsc`', () => { + const normalizer = new RSCPathnameNormalizer(true) + const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + + it('should return true if it matches', () => { + const normalizer = new RSCPathnameNormalizer(true) + const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(true) + } + }) + + it('should return false if it is disabled but ends with .rsc', () => { + const normalizer = new RSCPathnameNormalizer(false) + const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + }) + + describe('normalize', () => { + it('should return the same pathname if we are not matched and the pathname does not end with `.rsc`', () => { + const normalizer = new RSCPathnameNormalizer(true) + const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname) + } + }) + + it('should strip the `.rsc` extension from the pathname when it matches', () => { + const normalizer = new RSCPathnameNormalizer(true) + const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] + const expected = ['/foo', '/foo/bar', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe( + expected[pathnames.indexOf(pathname)] + ) + } + }) + + it('should return the same pathname if it is disabled but ends with .rsc', () => { + const normalizer = new RSCPathnameNormalizer(false) + const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname) + } + }) + }) +}) diff --git a/packages/next/src/server/future/normalizers/request/rsc.ts b/packages/next/src/server/future/normalizers/request/rsc.ts new file mode 100644 index 0000000000000..a56a0819fc9b4 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/rsc.ts @@ -0,0 +1,25 @@ +import type { Normalizer } from '../normalizer' + +export class RSCPathnameNormalizer implements Normalizer { + constructor(private readonly hasAppDir: boolean) {} + + public match(pathname: string) { + // If there's no app directory, we don't match. + if (!this.hasAppDir) return false + + // If the pathname doesn't end in `.rsc`, we don't match. + if (!pathname.endsWith('.rsc')) return false + + return true + } + + public normalize(pathname: string, matched?: boolean): string { + // If there's no app directory, we don't need to normalize. + if (!this.hasAppDir) return pathname + + // If we're not matched and we don't match, we don't need to normalize. + if (!matched && !this.match(pathname)) return pathname + + return pathname.substring(0, pathname.length - 4) + } +} diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index 6986a891509c3..05b97245eb5ce 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -39,6 +39,7 @@ import { } from '../../../shared/lib/constants' import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' import { normalizeMetadataRoute } from '../../../lib/metadata/get-metadata-route' +import { RSCPathnameNormalizer } from '../../future/normalizers/request/rsc' export type FsOutput = { type: @@ -367,6 +368,12 @@ export async function setupFsCheck(opts: { let ensureFn: (item: FsOutput) => Promise | undefined + const normalizers = { + // Because we can't know if the app directory is enabled or not at this + // stage, we assume that it is. + rsc: new RSCPathnameNormalizer(true), + } + return { headers, rewrites, @@ -402,10 +409,10 @@ export async function setupFsCheck(opts: { return lruResult } - // handle minimal mode case with .rsc output path (this is - // mostly for testings) - if (opts.minimalMode && itemPath.endsWith('.rsc')) { - itemPath = itemPath.substring(0, itemPath.length - '.rsc'.length) + // Handle minimal mode case with .rsc output path (this is + // mostly for testing). + if (opts.minimalMode && normalizers.rsc.match(itemPath)) { + itemPath = normalizers.rsc.normalize(itemPath, true) } const { basePath } = opts.config diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index ed8b901728506..1cbaad8cbb97c 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -10,6 +10,7 @@ import type { UnwrapPromise } from '../../../lib/coalesced-function' import type { NextUrlWithParsedQuery } from '../../request-meta' import url from 'url' +import path from 'node:path' import setupDebug from 'next/dist/compiled/debug' import { getCloneableBody } from '../../body-streams' import { filterReqHeaders, ipcForbiddenHeaders } from '../server-ipc/utils' @@ -26,6 +27,8 @@ import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix' import { detectDomainLocale } from '../../../shared/lib/i18n/detect-domain-locale' import { normalizeLocalePath } from '../../../shared/lib/i18n/normalize-locale-path' import { removePathPrefix } from '../../../shared/lib/router/utils/remove-path-prefix' +import { NextDataPathnameNormalizer } from '../../future/normalizers/request/next-data' +import { BasePathPathnameNormalizer } from '../../future/normalizers/request/base-path' import { addRequestMeta } from '../../request-meta' import { @@ -289,6 +292,11 @@ export function getResolveRoutes( } } + const normalizers = { + basePath: new BasePathPathnameNormalizer(config.basePath), + data: new NextDataPathnameNormalizer(fsChecker.buildId), + } + async function handleRoute( route: (typeof routes)[0] ): Promise> | void> { @@ -354,33 +362,28 @@ export function getResolveRoutes( } } - if (route.name === 'middleware_next_data') { + if (route.name === 'middleware_next_data' && parsedUrl.pathname) { if (fsChecker.getMiddlewareMatchers()?.length) { - const nextDataPrefix = addPathPrefix( - `/_next/data/${fsChecker.buildId}/`, - config.basePath - ) + let normalized = parsedUrl.pathname - if ( - parsedUrl.pathname?.startsWith(nextDataPrefix) && - parsedUrl.pathname.endsWith('.json') - ) { + // Remove the base path if it exists. + const hadBasePath = normalizers.basePath.match(parsedUrl.pathname) + if (hadBasePath) { + normalized = normalizers.basePath.normalize(normalized, true) + } + + if (normalizers.data.match(normalized)) { parsedUrl.query.__nextDataReq = '1' - parsedUrl.pathname = parsedUrl.pathname.substring( - nextDataPrefix.length - 1 - ) - parsedUrl.pathname = parsedUrl.pathname.substring( - 0, - parsedUrl.pathname.length - '.json'.length - ) - parsedUrl.pathname = addPathPrefix( - parsedUrl.pathname || '', - config.basePath - ) - parsedUrl.pathname = - parsedUrl.pathname === '/index' ? '/' : parsedUrl.pathname - - parsedUrl.pathname = maybeAddTrailingSlash(parsedUrl.pathname) + normalized = normalizers.data.normalize(normalized, true) + + if (hadBasePath) { + normalized = path.posix.join(config.basePath, normalized) + } + + // Re-add the trailing slash (if required). + normalized = maybeAddTrailingSlash(normalized) + + parsedUrl.pathname = normalized } } } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index b987f1021358e..ee70b421cbfb7 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -769,6 +769,7 @@ export default class NextNodeServer extends BaseServer { await this.render404(req, res) return true } + const paramsResult = ImageOptimizerCache.validateParams( (req as NodeNextRequest).originalRequest, parsedUrl.query, @@ -781,6 +782,7 @@ export default class NextNodeServer extends BaseServer { res.body(paramsResult.errorMessage).send() return true } + const cacheKey = ImageOptimizerCache.getCacheKey(paramsResult) try { @@ -816,6 +818,7 @@ export default class NextNodeServer extends BaseServer { 'invariant did not get entry from image response cache' ) } + sendResponse( (req as NodeNextRequest).originalRequest, (res as NodeNextResponse).originalResponse, @@ -828,6 +831,7 @@ export default class NextNodeServer extends BaseServer { cacheEntry.revalidate || 0, Boolean(this.renderOpts.dev) ) + return true } catch (err) { if (err instanceof ImageError) { res.statusCode = err.statusCode @@ -836,7 +840,6 @@ export default class NextNodeServer extends BaseServer { } throw err } - return true } } diff --git a/packages/next/src/server/server-utils.ts b/packages/next/src/server/server-utils.ts index 495e7ff3c6e08..7daeaf366b41f 100644 --- a/packages/next/src/server/server-utils.ts +++ b/packages/next/src/server/server-utils.ts @@ -15,7 +15,7 @@ import { prepareDestination, } from '../shared/lib/router/utils/prepare-destination' import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' -import { normalizeRscPath } from '../shared/lib/router/utils/app-paths' +import { normalizeRscURL } from '../shared/lib/router/utils/app-paths' import { NEXT_QUERY_PARAM_PREFIX } from '../lib/constants' export function normalizeVercelUrl( @@ -337,12 +337,12 @@ export function getUtils({ let value: string | string[] | undefined = params[key] if (typeof value === 'string') { - value = normalizeRscPath(value, true) + value = normalizeRscURL(value) } if (Array.isArray(value)) { value = value.map((val) => { if (typeof val === 'string') { - val = normalizeRscPath(val, true) + val = normalizeRscURL(val) } return val }) diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index d14a8486fae91..36b074208f4c1 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -10,7 +10,7 @@ import { relativizeURL } from '../../shared/lib/router/utils/relativize-url' import { waitUntilSymbol } from './spec-extension/fetch-event' import { NextURL } from './next-url' import { stripInternalSearchParams } from '../internal-utils' -import { normalizeRscPath } from '../../shared/lib/router/utils/app-paths' +import { normalizeRscURL } from '../../shared/lib/router/utils/app-paths' import { NEXT_ROUTER_PREFETCH, NEXT_ROUTER_STATE_TREE, @@ -72,7 +72,7 @@ export async function adapter( ? JSON.parse(self.__PRERENDER_MANIFEST) : undefined - params.request.url = normalizeRscPath(params.request.url, true) + params.request.url = normalizeRscURL(params.request.url) const requestUrl = new NextURL(params.request.url, { headers: params.request.headers, diff --git a/packages/next/src/shared/lib/router/utils/app-paths.test.ts b/packages/next/src/shared/lib/router/utils/app-paths.test.ts index a9341ba272d80..f815af3deaa18 100644 --- a/packages/next/src/shared/lib/router/utils/app-paths.test.ts +++ b/packages/next/src/shared/lib/router/utils/app-paths.test.ts @@ -1,22 +1,10 @@ -import { normalizeRscPath } from './app-paths' +import { normalizeRscURL } from './app-paths' describe('normalizeRscPath', () => { - describe('enabled', () => { - it('should normalize url with .rsc', () => { - expect(normalizeRscPath('/test.rsc', true)).toBe('/test') - }) - it('should normalize url with .rsc and searchparams', () => { - expect(normalizeRscPath('/test.rsc?abc=def', true)).toBe('/test?abc=def') - }) + it('should normalize url with .rsc', () => { + expect(normalizeRscURL('/test.rsc')).toBe('/test') }) - describe('disabled', () => { - it('should normalize url with .rsc', () => { - expect(normalizeRscPath('/test.rsc', false)).toBe('/test.rsc') - }) - it('should normalize url with .rsc and searchparams', () => { - expect(normalizeRscPath('/test.rsc?abc=def', false)).toBe( - '/test.rsc?abc=def' - ) - }) + it('should normalize url with .rsc and searchparams', () => { + expect(normalizeRscURL('/test.rsc?abc=def')).toBe('/test?abc=def') }) }) diff --git a/packages/next/src/shared/lib/router/utils/app-paths.ts b/packages/next/src/shared/lib/router/utils/app-paths.ts index 77612edf6d10a..fa7a80a4f6731 100644 --- a/packages/next/src/shared/lib/router/utils/app-paths.ts +++ b/packages/next/src/shared/lib/router/utils/app-paths.ts @@ -55,12 +55,10 @@ export function normalizeAppPath(route: string) { * Strips the `.rsc` extension if it's in the pathname. * Since this function is used on full urls it checks `?` for searchParams handling. */ -export function normalizeRscPath(pathname: string, enabled?: boolean) { - return enabled - ? pathname.replace( - /\.rsc($|\?)/, - // $1 ensures `?` is preserved - '$1' - ) - : pathname +export function normalizeRscURL(url: string) { + return url.replace( + /\.rsc($|\?)/, + // $1 ensures `?` is preserved + '$1' + ) }