| | import type { __ApiPreviewProps } from './api-utils' |
| | import type { |
| | GenericComponentMod, |
| | LoadComponentsReturnType, |
| | } from './load-components' |
| | import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher' |
| | import type { Params } from './request/params' |
| | import type { NextConfig, NextConfigRuntime } from './config-shared' |
| | import { |
| | DEFAULT_MAX_POSTPONED_STATE_SIZE, |
| | parseMaxPostponedStateSize, |
| | } from './config-shared' |
| | import type { |
| | NextParsedUrlQuery, |
| | NextUrlWithParsedQuery, |
| | RequestMeta, |
| | } from './request-meta' |
| | import type { ParsedUrlQuery } from 'querystring' |
| | import type { RenderOptsPartial as PagesRenderOptsPartial } from './render' |
| | import type { |
| | RenderOptsPartial as AppRenderOptsPartial, |
| | ServerOnInstrumentationRequestError, |
| | } from './app-render/types' |
| | import type { ServerComponentsHmrCache } from './response-cache' |
| | import { |
| | NormalizeError, |
| | DecodeError, |
| | normalizeRepeatedSlashes, |
| | MissingStaticPage, |
| | } from '../shared/lib/utils' |
| | import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' |
| | import type { BaseNextRequest, BaseNextResponse } from './base-http' |
| | import type { |
| | ManifestRewriteRoute, |
| | ManifestRoute, |
| | PrerenderManifest, |
| | } from '../build' |
| | import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin' |
| | import type { NextFontManifest } from '../build/webpack/plugins/next-font-manifest-plugin' |
| | import type { PagesAPIRouteMatch } from './route-matches/pages-api-route-match' |
| | import type { |
| | Server as HTTPServer, |
| | IncomingMessage, |
| | ServerResponse as HTTPServerResponse, |
| | } from 'http' |
| | import type { ProxyMatcher } from '../build/analysis/get-page-static-info' |
| | import type { TLSSocket } from 'tls' |
| | import type { PathnameNormalizer } from './normalizers/request/pathname-normalizer' |
| | import type { InstrumentationModule } from './instrumentation/types' |
| |
|
| | import * as path from 'path' |
| | import { format as formatUrl } from 'url' |
| | import { formatHostname } from './lib/format-hostname' |
| | import { |
| | APP_PATHS_MANIFEST, |
| | NEXT_BUILTIN_DOCUMENT, |
| | PAGES_MANIFEST, |
| | STATIC_STATUS_PAGES, |
| | UNDERSCORE_NOT_FOUND_ROUTE, |
| | UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, |
| | } from '../shared/lib/constants' |
| | import { isDynamicRoute } from '../shared/lib/router/utils' |
| | import { execOnce } from '../shared/lib/utils' |
| | import { isBlockedPage } from './utils' |
| | import { getBotType, isBot } from '../shared/lib/router/utils/is-bot' |
| | import RenderResult from './render-result' |
| | import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' |
| | import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' |
| | import * as Log from '../build/output/log' |
| | import { getServerUtils } from './server-utils' |
| | import isError, { getProperError } from '../lib/is-error' |
| | import { |
| | addRequestMeta, |
| | getRequestMeta, |
| | removeRequestMeta, |
| | setRequestMeta, |
| | } from './request-meta' |
| | import { removePathPrefix } from '../shared/lib/router/utils/remove-path-prefix' |
| | import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' |
| | import { getHostname } from '../shared/lib/get-hostname' |
| | import { |
| | parseUrl, |
| | parseUrl as parseUrlUtil, |
| | } from '../shared/lib/router/utils/parse-url' |
| | import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' |
| | import { |
| | RSC_HEADER, |
| | NEXT_RSC_UNION_QUERY, |
| | NEXT_ROUTER_PREFETCH_HEADER, |
| | NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, |
| | NEXT_URL, |
| | NEXT_ROUTER_STATE_TREE_HEADER, |
| | } from '../client/components/app-router-headers' |
| | import type { |
| | MatchOptions, |
| | RouteMatcherManager, |
| | } from './route-matcher-managers/route-matcher-manager' |
| | import { LocaleRouteNormalizer } from './normalizers/locale-route-normalizer' |
| | import { DefaultRouteMatcherManager } from './route-matcher-managers/default-route-matcher-manager' |
| | import { AppPageRouteMatcherProvider } from './route-matcher-providers/app-page-route-matcher-provider' |
| | import { AppRouteRouteMatcherProvider } from './route-matcher-providers/app-route-route-matcher-provider' |
| | import { PagesAPIRouteMatcherProvider } from './route-matcher-providers/pages-api-route-matcher-provider' |
| | import { PagesRouteMatcherProvider } from './route-matcher-providers/pages-route-matcher-provider' |
| | import { ServerManifestLoader } from './route-matcher-providers/helpers/manifest-loaders/server-manifest-loader' |
| | import { |
| | getTracer, |
| | isBubbledError, |
| | SpanKind, |
| | SpanStatusCode, |
| | } from './lib/trace/tracer' |
| | import { BaseServerSpan } from './lib/trace/constants' |
| | import { I18NProvider } from './lib/i18n-provider' |
| | import { sendResponse } from './send-response' |
| | import { normalizeNextQueryParam } from './web/utils' |
| | import { |
| | HTML_CONTENT_TYPE_HEADER, |
| | JSON_CONTENT_TYPE_HEADER, |
| | MATCHED_PATH_HEADER, |
| | NEXT_RESUME_HEADER, |
| | } from '../lib/constants' |
| | import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' |
| | import { matchNextDataPathname } from './lib/match-next-data-pathname' |
| | import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path' |
| | import { RSCPathnameNormalizer } from './normalizers/request/rsc' |
| | import { stripFlightHeaders } from './app-render/strip-flight-headers' |
| | import { |
| | isAppPageRouteModule, |
| | isAppRouteRouteModule, |
| | } from './route-modules/checks' |
| | import { NextDataPathnameNormalizer } from './normalizers/request/next-data' |
| | import { getIsPossibleServerAction } from './lib/server-action-request-meta' |
| | import { isInterceptionRouteAppPath } from '../shared/lib/router/utils/interception-routes' |
| | import { toRoute } from './lib/to-route' |
| | import type { DeepReadonly } from '../shared/lib/deep-readonly' |
| | import { isNodeNextRequest, isNodeNextResponse } from './base-http/helpers' |
| | import { patchSetHeaderWithCookieSupport } from './lib/patch-set-header' |
| | import { checkIsAppPPREnabled } from './lib/experimental/ppr' |
| | import { |
| | getBuiltinRequestContext, |
| | type WaitUntil, |
| | } from './after/builtin-request-context' |
| | import { NextRequestHint } from './web/adapter' |
| | import type { RouteModule } from './route-modules/route-module' |
| | import { type FallbackMode, parseFallbackField } from '../lib/fallback' |
| | import { SegmentPrefixRSCPathnameNormalizer } from './normalizers/request/segment-prefix-rsc' |
| | import { shouldServeStreamingMetadata } from './lib/streaming-metadata' |
| | import { decodeQueryPathParameter } from './lib/decode-query-path-parameter' |
| | import { NoFallbackError } from '../shared/lib/no-fallback-error.external' |
| | import { fixMojibake } from './lib/fix-mojibake' |
| | import { computeCacheBustingSearchParam } from '../shared/lib/router/utils/cache-busting-search-param' |
| | import { setCacheBustingSearchParamWithHash } from '../client/components/router-reducer/set-cache-busting-search-param' |
| | import type { CacheControl } from './lib/cache-control' |
| | import type { PrerenderedRoute } from '../build/static-paths/types' |
| | import { createOpaqueFallbackRouteParams } from './request/fallback-params' |
| | import { RouteKind } from './route-kind' |
| | import type { ErrorModule } from './load-default-error-components' |
| |
|
| | export type FindComponentsResult< |
| | NextModule extends GenericComponentMod = GenericComponentMod, |
| | > = { |
| | components: LoadComponentsReturnType<NextModule> |
| | query: NextParsedUrlQuery |
| | } |
| |
|
| | export interface MiddlewareRoutingItem { |
| | page: string |
| | match: MiddlewareRouteMatch |
| | matchers?: ProxyMatcher[] |
| | } |
| |
|
| | export type RouteHandler< |
| | ServerRequest extends BaseNextRequest = BaseNextRequest, |
| | ServerResponse extends BaseNextResponse = BaseNextResponse, |
| | > = ( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | parsedUrl: NextUrlWithParsedQuery |
| | ) => PromiseLike<boolean> | boolean |
| |
|
| | |
| | |
| | |
| | |
| | export type NormalizedRouteManifest = { |
| | readonly dynamicRoutes: ReadonlyArray<ManifestRoute> |
| | readonly rewrites: { |
| | readonly beforeFiles: ReadonlyArray<ManifestRewriteRoute> |
| | readonly afterFiles: ReadonlyArray<ManifestRewriteRoute> |
| | readonly fallback: ReadonlyArray<ManifestRewriteRoute> |
| | } |
| | } |
| |
|
| | export interface Options { |
| | |
| | |
| | |
| | conf: NextConfig |
| | |
| | |
| | |
| | customServer?: boolean |
| | |
| | |
| | |
| | dev?: boolean |
| | |
| | |
| | |
| | experimentalTestProxy?: boolean |
| |
|
| | |
| | |
| | |
| | experimentalHttpsServer?: boolean |
| | |
| | |
| | |
| | dir?: string |
| | |
| | |
| | |
| | minimalMode?: boolean |
| | |
| | |
| | |
| | quiet?: boolean |
| | |
| | |
| | |
| | hostname?: string |
| | |
| | |
| | |
| | port?: number |
| | |
| | |
| | |
| | httpServer?: HTTPServer |
| | } |
| |
|
| | export type RenderOpts = PagesRenderOptsPartial & AppRenderOptsPartial |
| |
|
| | export type LoadedRenderOpts< |
| | NextModule extends GenericComponentMod = GenericComponentMod, |
| | > = RenderOpts & LoadComponentsReturnType<NextModule> & RequestLifecycleOpts |
| |
|
| | export type RequestLifecycleOpts = { |
| | waitUntil: ((promise: Promise<any>) => void) | undefined |
| | onClose: (callback: () => void) => void |
| | onAfterTaskError: ((error: unknown) => void) | undefined |
| | } |
| |
|
| | type BaseRenderOpts = RenderOpts & { |
| | poweredByHeader: boolean |
| | generateEtags: boolean |
| | previewProps: __ApiPreviewProps |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export interface BaseRequestHandler< |
| | ServerRequest extends BaseNextRequest | IncomingMessage = BaseNextRequest, |
| | ServerResponse extends |
| | | BaseNextResponse |
| | | HTTPServerResponse = BaseNextResponse, |
| | > { |
| | ( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | parsedUrl?: NextUrlWithParsedQuery | undefined |
| | ): Promise<void> | void |
| | } |
| |
|
| | export type RequestContext< |
| | ServerRequest extends BaseNextRequest = BaseNextRequest, |
| | ServerResponse extends BaseNextResponse = BaseNextResponse, |
| | > = { |
| | req: ServerRequest |
| | res: ServerResponse |
| | pathname: string |
| | query: NextParsedUrlQuery |
| | renderOpts: RenderOpts |
| | } |
| |
|
| | |
| | |
| | export class WrappedBuildError extends Error { |
| | innerError: Error |
| |
|
| | constructor(innerError: Error) { |
| | super() |
| | this.innerError = innerError |
| | } |
| | } |
| |
|
| | type ResponsePayload = { |
| | body: RenderResult |
| | cacheControl?: CacheControl |
| | } |
| |
|
| | export type NextEnabledDirectories = { |
| | readonly pages: boolean |
| | readonly app: boolean |
| | } |
| |
|
| | export default abstract class Server< |
| | ServerOptions extends Options = Options, |
| | ServerRequest extends BaseNextRequest = BaseNextRequest, |
| | ServerResponse extends BaseNextResponse = BaseNextResponse, |
| | > { |
| | public readonly hostname?: string |
| | public readonly fetchHostname?: string |
| | public readonly port?: number |
| | protected readonly dir: string |
| | protected readonly quiet: boolean |
| | protected readonly nextConfig: NextConfigRuntime |
| | protected readonly distDir: string |
| | protected readonly publicDir: string |
| | protected readonly hasStaticDir: boolean |
| | protected readonly pagesManifest?: PagesManifest |
| | protected readonly appPathsManifest?: PagesManifest |
| | protected readonly buildId: string |
| | protected readonly minimalMode: boolean |
| | protected readonly renderOpts: BaseRenderOpts |
| | protected readonly serverOptions: Readonly<ServerOptions> |
| | protected readonly appPathRoutes?: Record<string, string[]> |
| | protected readonly clientReferenceManifest?: DeepReadonly<ClientReferenceManifest> |
| | protected interceptionRoutePatterns: RegExp[] |
| | protected nextFontManifest?: DeepReadonly<NextFontManifest> |
| | protected instrumentation: InstrumentationModule | undefined |
| |
|
| | protected abstract getPublicDir(): string |
| | protected abstract getHasStaticDir(): boolean |
| | protected abstract getPagesManifest(): PagesManifest | undefined |
| | protected abstract getAppPathsManifest(): PagesManifest | undefined |
| | protected abstract getBuildId(): string |
| | protected abstract getinterceptionRoutePatterns(): RegExp[] |
| |
|
| | protected readonly enabledDirectories: NextEnabledDirectories |
| | protected abstract getEnabledDirectories(dev: boolean): NextEnabledDirectories |
| |
|
| | protected readonly experimentalTestProxy?: boolean |
| |
|
| | protected abstract findPageComponents(params: { |
| | locale: string | undefined |
| | page: string |
| | query: NextParsedUrlQuery |
| | params: Params |
| | isAppPath: boolean |
| | |
| | |
| | sriEnabled?: boolean |
| | appPaths?: ReadonlyArray<string> | null |
| | shouldEnsure?: boolean |
| | url?: string |
| | }): Promise<FindComponentsResult | null> |
| | protected abstract getPrerenderManifest(): DeepReadonly<PrerenderManifest> |
| | protected abstract getNextFontManifest(): |
| | | DeepReadonly<NextFontManifest> |
| | | undefined |
| | protected abstract attachRequestMeta( |
| | req: ServerRequest, |
| | parsedUrl: NextUrlWithParsedQuery |
| | ): void |
| | protected abstract hasPage(pathname: string): Promise<boolean> |
| |
|
| | protected abstract sendRenderResult( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | options: { |
| | result: RenderResult |
| | generateEtags: boolean |
| | poweredByHeader: boolean |
| | cacheControl: CacheControl | undefined |
| | cdnCacheControlHeader?: string |
| | } |
| | ): Promise<void> |
| |
|
| | protected abstract runApi( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | query: ParsedUrlQuery, |
| | match: PagesAPIRouteMatch |
| | ): Promise<boolean> |
| |
|
| | protected abstract renderHTML( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: NextParsedUrlQuery, |
| | renderOpts: LoadedRenderOpts |
| | ): Promise<RenderResult> |
| |
|
| | protected abstract getIncrementalCache(options: { |
| | requestHeaders: Record<string, undefined | string | string[]> |
| | }): Promise<import('./lib/incremental-cache').IncrementalCache> |
| |
|
| | protected getServerComponentsHmrCache(): |
| | | ServerComponentsHmrCache |
| | | undefined { |
| | return this.nextConfig.experimental.serverComponentsHmrCache |
| | ? (globalThis as any).__serverComponentsHmrCache |
| | : undefined |
| | } |
| |
|
| | protected abstract loadEnvConfig(params: { |
| | dev: boolean |
| | forceReload: boolean |
| | }): void |
| |
|
| | |
| | public readonly matchers: RouteMatcherManager |
| | protected readonly i18nProvider?: I18NProvider |
| | protected readonly localeNormalizer?: LocaleRouteNormalizer |
| |
|
| | protected readonly normalizers: { |
| | readonly rsc: RSCPathnameNormalizer | undefined |
| | readonly segmentPrefetchRSC: SegmentPrefixRSCPathnameNormalizer | undefined |
| | readonly data: NextDataPathnameNormalizer | undefined |
| | } |
| |
|
| | private readonly isAppPPREnabled: boolean |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | public constructor(options: ServerOptions) { |
| | const { |
| | dir = '.', |
| | quiet = false, |
| | conf, |
| | dev = false, |
| | minimalMode = false, |
| | hostname, |
| | port, |
| | experimentalTestProxy, |
| | } = options |
| |
|
| | this.experimentalTestProxy = experimentalTestProxy |
| | this.serverOptions = options |
| |
|
| | this.dir = path.resolve( dir) |
| |
|
| | this.quiet = quiet |
| | this.loadEnvConfig({ dev, forceReload: false }) |
| |
|
| | |
| | |
| | this.nextConfig = conf as NextConfigRuntime |
| |
|
| | let deploymentId |
| | if (this.nextConfig.experimental.runtimeServerDeploymentId) { |
| | if (!process.env.NEXT_DEPLOYMENT_ID) { |
| | throw new Error( |
| | 'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled' |
| | ) |
| | } |
| | deploymentId = process.env.NEXT_DEPLOYMENT_ID |
| | } else { |
| | let id = this.nextConfig.experimental.useSkewCookie |
| | ? '' |
| | : this.nextConfig.deploymentId || '' |
| |
|
| | deploymentId = id |
| | process.env.NEXT_DEPLOYMENT_ID = id |
| | } |
| |
|
| | this.hostname = hostname |
| | if (this.hostname) { |
| | |
| | this.fetchHostname = formatHostname(this.hostname) |
| | } |
| | this.port = port |
| | this.distDir = path.join( |
| | this.dir, |
| | this.nextConfig.distDir |
| | ) |
| | this.publicDir = this.getPublicDir() |
| | this.hasStaticDir = !minimalMode && this.getHasStaticDir() |
| |
|
| | this.i18nProvider = this.nextConfig.i18n?.locales |
| | ? new I18NProvider(this.nextConfig.i18n) |
| | : undefined |
| |
|
| | |
| | this.localeNormalizer = this.i18nProvider |
| | ? new LocaleRouteNormalizer(this.i18nProvider) |
| | : undefined |
| |
|
| | const { assetPrefix, generateEtags } = this.nextConfig |
| |
|
| | this.buildId = this.getBuildId() |
| | |
| | |
| | const minimalModeKey = 'minimalMode' |
| | this[minimalModeKey] = |
| | minimalMode || !!process.env.NEXT_PRIVATE_MINIMAL_MODE |
| |
|
| | this.enabledDirectories = this.getEnabledDirectories(dev) |
| |
|
| | this.isAppPPREnabled = |
| | this.enabledDirectories.app && |
| | checkIsAppPPREnabled(this.nextConfig.experimental.ppr) |
| |
|
| | this.normalizers = { |
| | |
| | |
| | |
| | rsc: |
| | this.enabledDirectories.app && this.minimalMode |
| | ? new RSCPathnameNormalizer() |
| | : undefined, |
| | segmentPrefetchRSC: this.minimalMode |
| | ? new SegmentPrefixRSCPathnameNormalizer() |
| | : undefined, |
| | data: this.enabledDirectories.pages |
| | ? new NextDataPathnameNormalizer(this.buildId) |
| | : undefined, |
| | } |
| |
|
| | this.nextFontManifest = this.getNextFontManifest() |
| |
|
| | this.renderOpts = { |
| | dir: this.dir, |
| | supportsDynamicResponse: true, |
| | trailingSlash: this.nextConfig.trailingSlash, |
| | deploymentId: deploymentId, |
| | poweredByHeader: this.nextConfig.poweredByHeader, |
| | generateEtags, |
| | previewProps: this.getPrerenderManifest().preview, |
| | basePath: this.nextConfig.basePath, |
| | images: this.nextConfig.images, |
| | optimizeCss: this.nextConfig.experimental.optimizeCss, |
| | nextConfigOutput: this.nextConfig.output, |
| | nextScriptWorkers: this.nextConfig.experimental.nextScriptWorkers, |
| | disableOptimizedLoading: |
| | this.nextConfig.experimental.disableOptimizedLoading, |
| | domainLocales: this.nextConfig.i18n?.domains, |
| | distDir: this.distDir, |
| | serverComponents: this.enabledDirectories.app, |
| | cacheLifeProfiles: this.nextConfig.cacheLife, |
| | enableTainting: this.nextConfig.experimental.taint, |
| | crossOrigin: this.nextConfig.crossOrigin |
| | ? this.nextConfig.crossOrigin |
| | : undefined, |
| | largePageDataBytes: this.nextConfig.experimental.largePageDataBytes, |
| |
|
| | isExperimentalCompile: this.nextConfig.experimental.isExperimentalCompile, |
| | |
| | htmlLimitedBots: this.nextConfig.htmlLimitedBots, |
| | cacheComponents: this.nextConfig.cacheComponents ?? false, |
| | experimental: { |
| | expireTime: this.nextConfig.expireTime, |
| | staleTimes: this.nextConfig.experimental.staleTimes, |
| | clientTraceMetadata: this.nextConfig.experimental.clientTraceMetadata, |
| | clientParamParsingOrigins: |
| | this.nextConfig.experimental.clientParamParsingOrigins, |
| | dynamicOnHover: this.nextConfig.experimental.dynamicOnHover ?? false, |
| | inlineCss: this.nextConfig.experimental.inlineCss ?? false, |
| | authInterrupts: !!this.nextConfig.experimental.authInterrupts, |
| | cdnCacheControlHeader: |
| | this.nextConfig.experimental.cdnCacheControlHeader, |
| | maxPostponedStateSizeBytes: parseMaxPostponedStateSize( |
| | this.nextConfig.experimental.maxPostponedStateSize |
| | ), |
| | }, |
| | onInstrumentationRequestError: |
| | this.instrumentationOnRequestError.bind(this), |
| | reactMaxHeadersLength: this.nextConfig.reactMaxHeadersLength, |
| | } |
| |
|
| | this.pagesManifest = this.getPagesManifest() |
| | this.appPathsManifest = this.getAppPathsManifest() |
| | this.appPathRoutes = this.getAppPathRoutes() |
| | this.interceptionRoutePatterns = this.getinterceptionRoutePatterns() |
| |
|
| | |
| | this.matchers = this.getRouteMatchers() |
| |
|
| | |
| | |
| | |
| | void this.matchers.reload() |
| |
|
| | this.setAssetPrefix(assetPrefix) |
| | } |
| |
|
| | protected reloadMatchers() { |
| | return this.matchers.reload() |
| | } |
| |
|
| | private handleRSCRequest: RouteHandler<ServerRequest, ServerResponse> = ( |
| | req, |
| | _res, |
| | parsedUrl |
| | ) => { |
| | if (!parsedUrl.pathname) return false |
| |
|
| | if (this.normalizers.segmentPrefetchRSC?.match(parsedUrl.pathname)) { |
| | const result = this.normalizers.segmentPrefetchRSC.extract( |
| | parsedUrl.pathname |
| | ) |
| | if (!result) return false |
| |
|
| | const { originalPathname, segmentPath } = result |
| | parsedUrl.pathname = originalPathname |
| |
|
| | |
| | req.headers[RSC_HEADER] = '1' |
| | req.headers[NEXT_ROUTER_PREFETCH_HEADER] = '1' |
| | req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] = segmentPath |
| |
|
| | addRequestMeta(req, 'isRSCRequest', true) |
| | addRequestMeta(req, 'isPrefetchRSCRequest', true) |
| | addRequestMeta(req, 'segmentPrefetchRSCRequest', segmentPath) |
| | } else if (this.normalizers.rsc?.match(parsedUrl.pathname)) { |
| | parsedUrl.pathname = this.normalizers.rsc.normalize( |
| | parsedUrl.pathname, |
| | true |
| | ) |
| |
|
| | |
| | req.headers[RSC_HEADER] = '1' |
| | addRequestMeta(req, 'isRSCRequest', true) |
| | } else if (req.headers['x-now-route-matches']) { |
| | |
| | |
| | |
| | |
| | |
| | stripFlightHeaders(req.headers) |
| |
|
| | return false |
| | } else if (req.headers[RSC_HEADER] === '1') { |
| | addRequestMeta(req, 'isRSCRequest', true) |
| |
|
| | if (req.headers[NEXT_ROUTER_PREFETCH_HEADER] === '1') { |
| | addRequestMeta(req, 'isPrefetchRSCRequest', true) |
| |
|
| | const segmentPrefetchRSCRequest = |
| | req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] |
| | if (typeof segmentPrefetchRSCRequest === 'string') { |
| | addRequestMeta( |
| | req, |
| | 'segmentPrefetchRSCRequest', |
| | segmentPrefetchRSCRequest |
| | ) |
| | } |
| | } |
| | } else { |
| | |
| | return false |
| | } |
| |
|
| | if (req.url) { |
| | const parsed = parseUrl(req.url) |
| | parsed.pathname = parsedUrl.pathname |
| | req.url = formatUrl(parsed) |
| | } |
| |
|
| | return false |
| | } |
| |
|
| | private handleNextDataRequest: RouteHandler<ServerRequest, ServerResponse> = |
| | async (req, res, parsedUrl) => { |
| | const middleware = await this.getMiddleware() |
| | const params = matchNextDataPathname(parsedUrl.pathname) |
| |
|
| | |
| | if (!params || !params.path) { |
| | return false |
| | } |
| |
|
| | if (params.path[0] !== this.buildId) { |
| | |
| | if (getRequestMeta(req, 'middlewareInvoke')) { |
| | return false |
| | } |
| |
|
| | |
| | await this.render404(req, res, parsedUrl) |
| | return true |
| | } |
| |
|
| | |
| | params.path.shift() |
| |
|
| | const lastParam = params.path[params.path.length - 1] |
| |
|
| | |
| | if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) { |
| | await this.render404(req, res, parsedUrl) |
| | return true |
| | } |
| |
|
| | |
| | let pathname = `/${params.path.join('/')}` |
| | pathname = getRouteFromAssetPath(pathname, '.json') |
| |
|
| | |
| | if (middleware) { |
| | if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) { |
| | pathname += '/' |
| | } |
| | if ( |
| | !this.nextConfig.trailingSlash && |
| | pathname.length > 1 && |
| | pathname.endsWith('/') |
| | ) { |
| | pathname = pathname.substring(0, pathname.length - 1) |
| | } |
| | } |
| |
|
| | if (this.i18nProvider) { |
| | |
| | const hostname = req?.headers.host?.split(':', 1)[0].toLowerCase() |
| |
|
| | const domainLocale = this.i18nProvider.detectDomainLocale(hostname) |
| | const defaultLocale = |
| | domainLocale?.defaultLocale ?? this.i18nProvider.config.defaultLocale |
| |
|
| | const localePathResult = this.i18nProvider.analyze(pathname) |
| |
|
| | |
| | |
| | if (localePathResult.detectedLocale) { |
| | pathname = localePathResult.pathname |
| | } |
| |
|
| | |
| | addRequestMeta(req, 'locale', localePathResult.detectedLocale) |
| | addRequestMeta(req, 'defaultLocale', defaultLocale) |
| |
|
| | |
| | |
| | if (!localePathResult.detectedLocale) { |
| | removeRequestMeta(req, 'localeInferredFromDefault') |
| | } |
| |
|
| | |
| | |
| | if (!localePathResult.detectedLocale && !middleware) { |
| | addRequestMeta(req, 'locale', defaultLocale) |
| | await this.render404(req, res, parsedUrl) |
| | return true |
| | } |
| | } |
| |
|
| | parsedUrl.pathname = pathname |
| | addRequestMeta(req, 'isNextDataReq', true) |
| |
|
| | return false |
| | } |
| |
|
| | protected handleNextImageRequest: RouteHandler< |
| | ServerRequest, |
| | ServerResponse |
| | > = () => false |
| |
|
| | protected handleCatchallRenderRequest: RouteHandler< |
| | ServerRequest, |
| | ServerResponse |
| | > = () => false |
| |
|
| | protected handleCatchallMiddlewareRequest: RouteHandler< |
| | ServerRequest, |
| | ServerResponse |
| | > = () => false |
| |
|
| | protected getRouteMatchers(): RouteMatcherManager { |
| | |
| | const manifestLoader = new ServerManifestLoader((name) => { |
| | switch (name) { |
| | case PAGES_MANIFEST: |
| | return this.getPagesManifest() ?? null |
| | case APP_PATHS_MANIFEST: |
| | return this.getAppPathsManifest() ?? null |
| | default: |
| | return null |
| | } |
| | }) |
| |
|
| | |
| | const matchers: RouteMatcherManager = new DefaultRouteMatcherManager() |
| |
|
| | |
| | matchers.push( |
| | new PagesRouteMatcherProvider( |
| | this.distDir, |
| | manifestLoader, |
| | this.i18nProvider |
| | ) |
| | ) |
| |
|
| | |
| | matchers.push( |
| | new PagesAPIRouteMatcherProvider( |
| | this.distDir, |
| | manifestLoader, |
| | this.i18nProvider |
| | ) |
| | ) |
| |
|
| | |
| | if (this.enabledDirectories.app) { |
| | |
| | matchers.push( |
| | new AppPageRouteMatcherProvider(this.distDir, manifestLoader) |
| | ) |
| | matchers.push( |
| | new AppRouteRouteMatcherProvider(this.distDir, manifestLoader) |
| | ) |
| | } |
| |
|
| | return matchers |
| | } |
| |
|
| | protected async instrumentationOnRequestError( |
| | ...args: Parameters<ServerOnInstrumentationRequestError> |
| | ) { |
| | const [err, req, ctx] = args |
| |
|
| | if (this.instrumentation) { |
| | try { |
| | await this.instrumentation.onRequestError?.( |
| | err, |
| | { |
| | path: req.url || '', |
| | method: req.method || 'GET', |
| | |
| | headers: |
| | req instanceof NextRequestHint |
| | ? Object.fromEntries(req.headers.entries()) |
| | : req.headers, |
| | }, |
| | ctx |
| | ) |
| | } catch (handlerErr) { |
| | |
| | console.error('Error in instrumentation.onRequestError:', handlerErr) |
| | } |
| | } |
| | } |
| |
|
| | public logError(err: unknown): void { |
| | if (this.quiet) return |
| | Log.error(err) |
| | } |
| |
|
| | public async handleRequest( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | parsedUrl?: NextUrlWithParsedQuery |
| | ): Promise<void> { |
| | await this.prepare() |
| | const method = req.method.toUpperCase() |
| | const tracer = getTracer() |
| |
|
| | return tracer.withPropagatedContext(req.headers, () => { |
| | return tracer.trace( |
| | BaseServerSpan.handleRequest, |
| | { |
| | spanName: `${method}`, |
| | kind: SpanKind.SERVER, |
| | attributes: { |
| | 'http.method': method, |
| | 'http.target': req.url, |
| | }, |
| | }, |
| | async (span) => |
| | this.handleRequestImpl(req, res, parsedUrl).finally(() => { |
| | if (!span) return |
| |
|
| | const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false |
| | span.setAttributes({ |
| | 'http.status_code': res.statusCode, |
| | 'next.rsc': isRSCRequest, |
| | }) |
| |
|
| | if (res.statusCode && res.statusCode >= 500) { |
| | |
| | |
| | span.setStatus({ |
| | code: SpanStatusCode.ERROR, |
| | }) |
| | |
| | span.setAttribute('error.type', res.statusCode.toString()) |
| | } |
| |
|
| | const rootSpanAttributes = tracer.getRootSpanAttributes() |
| | |
| | if (!rootSpanAttributes) return |
| |
|
| | if ( |
| | rootSpanAttributes.get('next.span_type') !== |
| | BaseServerSpan.handleRequest |
| | ) { |
| | console.warn( |
| | `Unexpected root span type '${rootSpanAttributes.get( |
| | 'next.span_type' |
| | )}'. Please report this Next.js issue https://github.com/vercel/next.js` |
| | ) |
| | return |
| | } |
| |
|
| | const route = rootSpanAttributes.get('next.route') |
| | if (route) { |
| | const name = isRSCRequest |
| | ? `RSC ${method} ${route}` |
| | : `${method} ${route}` |
| |
|
| | span.setAttributes({ |
| | 'next.route': route, |
| | 'http.route': route, |
| | 'next.span_name': name, |
| | }) |
| | span.updateName(name) |
| | } else { |
| | span.updateName(isRSCRequest ? `RSC ${method}` : `${method}`) |
| | } |
| | }) |
| | ) |
| | }) |
| | } |
| |
|
| | private async handleRequestImpl( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | parsedUrl?: NextUrlWithParsedQuery |
| | ): Promise<void> { |
| | try { |
| | |
| | await this.matchers.waitTillReady() |
| |
|
| | |
| | |
| | patchSetHeaderWithCookieSupport( |
| | req, |
| | isNodeNextResponse(res) ? res.originalResponse : res |
| | ) |
| |
|
| | const urlParts = (req.url || '').split('?', 1) |
| | const urlNoQuery = urlParts[0] |
| |
|
| | |
| | |
| | |
| | |
| | if (urlNoQuery?.match(/(\\|\/\/)/)) { |
| | const cleanUrl = normalizeRepeatedSlashes(req.url!) |
| | res.redirect(cleanUrl, 308).body(cleanUrl).send() |
| | return |
| | } |
| |
|
| | |
| | if (!parsedUrl || typeof parsedUrl !== 'object') { |
| | if (!req.url) { |
| | throw new Error('Invariant: url can not be undefined') |
| | } |
| |
|
| | parsedUrl = parseUrl(req.url) |
| | } |
| |
|
| | if (!parsedUrl.pathname) { |
| | throw new Error("Invariant: pathname can't be empty") |
| | } |
| |
|
| | |
| | if (typeof parsedUrl.query === 'string') { |
| | parsedUrl.query = Object.fromEntries( |
| | new URLSearchParams(parsedUrl.query) |
| | ) |
| | } |
| |
|
| | |
| | const { originalRequest = null } = isNodeNextRequest(req) ? req : {} |
| | const xForwardedProto = originalRequest?.headers['x-forwarded-proto'] |
| | const isHttps = xForwardedProto |
| | ? xForwardedProto === 'https' |
| | : !!(originalRequest?.socket as TLSSocket)?.encrypted |
| |
|
| | req.headers['x-forwarded-host'] ??= req.headers['host'] ?? this.hostname |
| | req.headers['x-forwarded-port'] ??= this.port |
| | ? this.port.toString() |
| | : isHttps |
| | ? '443' |
| | : '80' |
| | req.headers['x-forwarded-proto'] ??= isHttps ? 'https' : 'http' |
| | req.headers['x-forwarded-for'] ??= originalRequest?.socket?.remoteAddress |
| |
|
| | |
| | |
| | this.attachRequestMeta(req, parsedUrl) |
| |
|
| | let finished = await this.handleRSCRequest(req, res, parsedUrl) |
| | if (finished) return |
| |
|
| | const domainLocale = this.i18nProvider?.detectDomainLocale( |
| | getHostname(parsedUrl, req.headers) |
| | ) |
| |
|
| | const defaultLocale = |
| | domainLocale?.defaultLocale || this.nextConfig.i18n?.defaultLocale |
| | addRequestMeta(req, 'defaultLocale', defaultLocale) |
| |
|
| | const url = parseUrlUtil(req.url.replace(/^\/+/, '/')) |
| | const pathnameInfo = getNextPathnameInfo(url.pathname, { |
| | nextConfig: this.nextConfig, |
| | i18nProvider: this.i18nProvider, |
| | }) |
| | url.pathname = pathnameInfo.pathname |
| |
|
| | if (pathnameInfo.basePath) { |
| | req.url = removePathPrefix(req.url!, this.nextConfig.basePath) |
| | } |
| |
|
| | const useMatchedPathHeader = |
| | this.minimalMode && typeof req.headers[MATCHED_PATH_HEADER] === 'string' |
| |
|
| | |
| | if (useMatchedPathHeader) { |
| | try { |
| | if (this.enabledDirectories.app) { |
| | |
| | |
| | if (req.url.match(/^\/index($|\?)/)) { |
| | req.url = req.url.replace(/^\/index/, '/') |
| | } |
| | parsedUrl.pathname = |
| | parsedUrl.pathname === '/index' ? '/' : parsedUrl.pathname |
| | } |
| |
|
| | |
| | |
| | let { pathname: matchedPath } = new URL( |
| | fixMojibake(req.headers[MATCHED_PATH_HEADER] as string), |
| | 'http://localhost' |
| | ) |
| |
|
| | let { pathname: urlPathname } = new URL(req.url, 'http://localhost') |
| |
|
| | |
| | |
| | |
| | if (this.normalizers.data?.match(urlPathname)) { |
| | addRequestMeta(req, 'isNextDataReq', true) |
| | } |
| |
|
| | |
| | |
| | if ( |
| | this.isAppPPREnabled && |
| | this.minimalMode && |
| | req.headers[NEXT_RESUME_HEADER] === '1' && |
| | req.method === 'POST' |
| | ) { |
| | |
| | const maxPostponedStateSize = |
| | this.nextConfig.experimental.maxPostponedStateSize ?? |
| | DEFAULT_MAX_POSTPONED_STATE_SIZE |
| | const maxPostponedStateSizeBytes = parseMaxPostponedStateSize( |
| | this.nextConfig.experimental.maxPostponedStateSize |
| | ) |
| | if (maxPostponedStateSizeBytes === undefined) { |
| | throw new Error( |
| | 'maxPostponedStateSize must be a valid number (bytes) or filesize format string (e.g., "5mb")' |
| | ) |
| | } |
| |
|
| | |
| | |
| | |
| | const body: Array<Buffer> = [] |
| | let size = 0 |
| | for await (const chunk of req.body) { |
| | size += Buffer.byteLength(chunk) |
| | if (size > maxPostponedStateSizeBytes) { |
| | res.statusCode = 413 |
| | const errorMessage = |
| | `Postponed state exceeded ${maxPostponedStateSize} limit. ` + |
| | `To configure the limit, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/max-postponed-state-size` |
| | res.body(errorMessage).send() |
| | return |
| | } |
| | body.push(chunk) |
| | } |
| | const postponed = Buffer.concat(body).toString('utf8') |
| |
|
| | addRequestMeta(req, 'postponed', postponed) |
| | } |
| |
|
| | |
| | |
| | if ( |
| | getRequestMeta(req, 'isNextDataReq') && |
| | getRequestMeta(req, 'postponed') |
| | ) { |
| | |
| | |
| | |
| | |
| | |
| | res.statusCode = 422 |
| | res.send() |
| | return |
| | } |
| |
|
| | matchedPath = this.normalize(matchedPath) |
| | const normalizedUrlPath = this.stripNextDataPath(urlPathname) |
| |
|
| | matchedPath = denormalizePagePath(matchedPath) |
| |
|
| | |
| | const localeAnalysisResult = this.i18nProvider?.analyze(matchedPath, { |
| | defaultLocale, |
| | }) |
| |
|
| | |
| | |
| | |
| | if (localeAnalysisResult) { |
| | addRequestMeta(req, 'locale', localeAnalysisResult.detectedLocale) |
| |
|
| | |
| | |
| | if (localeAnalysisResult.inferredFromDefault) { |
| | addRequestMeta(req, 'localeInferredFromDefault', true) |
| | } else { |
| | removeRequestMeta(req, 'localeInferredFromDefault') |
| | } |
| | } |
| |
|
| | let srcPathname = matchedPath |
| | let pageIsDynamic = isDynamicRoute(srcPathname) |
| | let paramsResult: { |
| | params: ParsedUrlQuery | false |
| | hasValidParams: boolean |
| | } = { |
| | params: false, |
| | hasValidParams: false, |
| | } |
| |
|
| | const match = await this.matchers.match(srcPathname, { |
| | i18n: localeAnalysisResult, |
| | }) |
| |
|
| | if (!pageIsDynamic && match) { |
| | |
| | srcPathname = match.definition.pathname |
| |
|
| | |
| | |
| | |
| | if (typeof match.params !== 'undefined') { |
| | pageIsDynamic = true |
| | paramsResult.params = match.params |
| | paramsResult.hasValidParams = true |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | if (localeAnalysisResult) { |
| | matchedPath = localeAnalysisResult.pathname |
| | } |
| |
|
| | const utils = getServerUtils({ |
| | pageIsDynamic, |
| | page: srcPathname, |
| | i18n: this.nextConfig.i18n, |
| | basePath: this.nextConfig.basePath, |
| | rewrites: this.getRoutesManifest()?.rewrites || { |
| | beforeFiles: [], |
| | afterFiles: [], |
| | fallback: [], |
| | }, |
| | caseSensitive: !!this.nextConfig.experimental.caseSensitiveRoutes, |
| | }) |
| |
|
| | |
| | |
| | if (defaultLocale && !pathnameInfo.locale) { |
| | parsedUrl.pathname = `/${defaultLocale}${parsedUrl.pathname}` |
| | } |
| |
|
| | |
| | |
| | const originQueryParams = { ...parsedUrl.query } |
| |
|
| | const pathnameBeforeRewrite = parsedUrl.pathname |
| | const { rewriteParams, rewrittenParsedUrl } = utils.handleRewrites( |
| | req, |
| | parsedUrl |
| | ) |
| | const rewriteParamKeys = Object.keys(rewriteParams) |
| |
|
| | |
| | |
| | |
| | const rewrittenQueryParams = { ...rewrittenParsedUrl.query } |
| | const didRewrite = |
| | pathnameBeforeRewrite !== rewrittenParsedUrl.pathname |
| |
|
| | if (didRewrite && rewrittenParsedUrl.pathname) { |
| | addRequestMeta(req, 'rewroteURL', rewrittenParsedUrl.pathname) |
| | } |
| |
|
| | const routeParamKeys = new Set<string>() |
| | for (const [key, value] of Object.entries(parsedUrl.query)) { |
| | const normalizedKey = normalizeNextQueryParam(key) |
| | if (!normalizedKey) continue |
| |
|
| | |
| | |
| | delete parsedUrl.query[key] |
| | routeParamKeys.add(normalizedKey) |
| |
|
| | if (typeof value === 'undefined') continue |
| |
|
| | rewrittenQueryParams[normalizedKey] = Array.isArray(value) |
| | ? value.map((v) => decodeQueryPathParameter(v)) |
| | : decodeQueryPathParameter(value) |
| | } |
| |
|
| | |
| | if (pageIsDynamic) { |
| | let params: ParsedUrlQuery | false = {} |
| |
|
| | |
| | |
| | if (!paramsResult.hasValidParams) { |
| | paramsResult = utils.normalizeDynamicRouteParams( |
| | rewrittenQueryParams, |
| | false |
| | ) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if ( |
| | !paramsResult.hasValidParams && |
| | !isDynamicRoute(normalizedUrlPath) |
| | ) { |
| | let matcherParams = utils.dynamicRouteMatcher?.(normalizedUrlPath) |
| |
|
| | if (matcherParams) { |
| | utils.normalizeDynamicRouteParams(matcherParams, false) |
| | Object.assign(paramsResult.params, matcherParams) |
| | paramsResult.hasValidParams = true |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if ( |
| | |
| | matchedPath !== '/index' && |
| | !paramsResult.hasValidParams && |
| | !isDynamicRoute(matchedPath) |
| | ) { |
| | let matcherParams = utils.dynamicRouteMatcher?.(matchedPath) |
| |
|
| | if (matcherParams) { |
| | const curParamsResult = utils.normalizeDynamicRouteParams( |
| | matcherParams, |
| | false |
| | ) |
| |
|
| | if (curParamsResult.hasValidParams) { |
| | Object.assign(params, matcherParams) |
| | paramsResult = curParamsResult |
| | } |
| | } |
| | } |
| |
|
| | if (paramsResult.hasValidParams) { |
| | params = paramsResult.params |
| | } |
| |
|
| | const routeMatchesHeader = req.headers['x-now-route-matches'] |
| | if ( |
| | typeof routeMatchesHeader === 'string' && |
| | routeMatchesHeader && |
| | isDynamicRoute(matchedPath) && |
| | !paramsResult.hasValidParams |
| | ) { |
| | const routeMatches = |
| | utils.getParamsFromRouteMatches(routeMatchesHeader) |
| |
|
| | if (routeMatches) { |
| | paramsResult = utils.normalizeDynamicRouteParams( |
| | routeMatches, |
| | true |
| | ) |
| |
|
| | if (paramsResult.hasValidParams) { |
| | params = paramsResult.params |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | if (!paramsResult.hasValidParams) { |
| | paramsResult = utils.normalizeDynamicRouteParams( |
| | rewrittenQueryParams, |
| | true |
| | ) |
| |
|
| | if (paramsResult.hasValidParams) { |
| | params = paramsResult.params |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | if ( |
| | utils.defaultRouteMatches && |
| | normalizedUrlPath === srcPathname && |
| | !paramsResult.hasValidParams |
| | ) { |
| | params = utils.defaultRouteMatches |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if (routeMatchesHeader === '') { |
| | addRequestMeta(req, 'renderFallbackShell', true) |
| | } |
| | } |
| |
|
| | if (params) { |
| | matchedPath = utils.interpolateDynamicPath(srcPathname, params) |
| | req.url = utils.interpolateDynamicPath(req.url!, params) |
| |
|
| | |
| | |
| | |
| | let segmentPrefetchRSCRequest = getRequestMeta( |
| | req, |
| | 'segmentPrefetchRSCRequest' |
| | ) |
| | if ( |
| | segmentPrefetchRSCRequest && |
| | isDynamicRoute(segmentPrefetchRSCRequest, false) |
| | ) { |
| | segmentPrefetchRSCRequest = utils.interpolateDynamicPath( |
| | segmentPrefetchRSCRequest, |
| | params |
| | ) |
| |
|
| | req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] = |
| | segmentPrefetchRSCRequest |
| | addRequestMeta( |
| | req, |
| | 'segmentPrefetchRSCRequest', |
| | segmentPrefetchRSCRequest |
| | ) |
| | } |
| | } |
| | } |
| |
|
| | if (pageIsDynamic || didRewrite) { |
| | utils.normalizeCdnUrl(req, [ |
| | ...rewriteParamKeys, |
| | ...Object.keys(utils.defaultRouteRegex?.groups || {}), |
| | ]) |
| | } |
| |
|
| | |
| | |
| | |
| | for (const key of routeParamKeys) { |
| | if (!(key in originQueryParams)) { |
| | delete parsedUrl.query[key] |
| | } |
| | } |
| |
|
| | parsedUrl.pathname = matchedPath |
| | url.pathname = parsedUrl.pathname |
| |
|
| | |
| | |
| | |
| | |
| | if ( |
| | match?.definition.kind === RouteKind.PAGES || |
| | match?.definition.kind === RouteKind.PAGES_API |
| | ) { |
| | parsedUrl.query = rewrittenQueryParams |
| | } |
| |
|
| | finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl) |
| | if (finished) return |
| | } catch (err) { |
| | if (err instanceof DecodeError || err instanceof NormalizeError) { |
| | res.statusCode = 400 |
| | return this.renderError(null, req, res, '/_error', {}) |
| | } |
| | throw err |
| | } |
| | } |
| |
|
| | addRequestMeta(req, 'isLocaleDomain', Boolean(domainLocale)) |
| |
|
| | if (pathnameInfo.locale) { |
| | req.url = formatUrl(url) |
| | addRequestMeta(req, 'didStripLocale', true) |
| | } |
| |
|
| | |
| | |
| | if (!this.minimalMode || !getRequestMeta(req, 'locale')) { |
| | |
| | if (pathnameInfo.locale) { |
| | addRequestMeta(req, 'locale', pathnameInfo.locale) |
| | } |
| | |
| | |
| | else if (defaultLocale) { |
| | addRequestMeta(req, 'locale', defaultLocale) |
| | addRequestMeta(req, 'localeInferredFromDefault', true) |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | if ( |
| | !(this.serverOptions as any).webServerConfig && |
| | !getRequestMeta(req, 'incrementalCache') |
| | ) { |
| | const incrementalCache = await this.getIncrementalCache({ |
| | requestHeaders: Object.assign({}, req.headers), |
| | }) |
| |
|
| | incrementalCache.resetRequestCache() |
| | addRequestMeta(req, 'incrementalCache', incrementalCache) |
| | |
| | |
| | ;(globalThis as any).__incrementalCache = incrementalCache |
| | } |
| |
|
| | |
| | |
| | if (!getRequestMeta(req, 'serverComponentsHmrCache')) { |
| | addRequestMeta( |
| | req, |
| | 'serverComponentsHmrCache', |
| | this.getServerComponentsHmrCache() |
| | ) |
| | } |
| |
|
| | |
| | |
| | |
| | const invokePath = getRequestMeta(req, 'invokePath') |
| | const useInvokePath = !useMatchedPathHeader && invokePath |
| |
|
| | if (useInvokePath) { |
| | const invokeStatus = getRequestMeta(req, 'invokeStatus') |
| | if (invokeStatus) { |
| | const invokeQuery = getRequestMeta(req, 'invokeQuery') |
| |
|
| | if (invokeQuery) { |
| | Object.assign(parsedUrl.query, invokeQuery) |
| | } |
| |
|
| | res.statusCode = invokeStatus |
| | let err: Error | null = getRequestMeta(req, 'invokeError') || null |
| |
|
| | return this.renderError(err, req, res, '/_error', parsedUrl.query) |
| | } |
| |
|
| | const parsedMatchedPath = new URL(invokePath || '/', 'http://n') |
| | const invokePathnameInfo = getNextPathnameInfo( |
| | parsedMatchedPath.pathname, |
| | { |
| | nextConfig: this.nextConfig, |
| | parseData: false, |
| | } |
| | ) |
| |
|
| | if (invokePathnameInfo.locale) { |
| | addRequestMeta(req, 'locale', invokePathnameInfo.locale) |
| | } |
| |
|
| | if (parsedUrl.pathname !== parsedMatchedPath.pathname) { |
| | parsedUrl.pathname = parsedMatchedPath.pathname |
| | addRequestMeta(req, 'rewroteURL', invokePathnameInfo.pathname) |
| | } |
| | const normalizeResult = normalizeLocalePath( |
| | removePathPrefix(parsedUrl.pathname, this.nextConfig.basePath || ''), |
| | this.nextConfig.i18n?.locales |
| | ) |
| |
|
| | if (normalizeResult.detectedLocale) { |
| | addRequestMeta(req, 'locale', normalizeResult.detectedLocale) |
| | } |
| | parsedUrl.pathname = normalizeResult.pathname |
| |
|
| | for (const key of Object.keys(parsedUrl.query)) { |
| | delete parsedUrl.query[key] |
| | } |
| | const invokeQuery = getRequestMeta(req, 'invokeQuery') |
| |
|
| | if (invokeQuery) { |
| | Object.assign(parsedUrl.query, invokeQuery) |
| | } |
| |
|
| | finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl) |
| | if (finished) return |
| |
|
| | await this.handleCatchallRenderRequest(req, res, parsedUrl) |
| | return |
| | } |
| |
|
| | if (getRequestMeta(req, 'middlewareInvoke')) { |
| | finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl) |
| | if (finished) return |
| |
|
| | finished = await this.handleCatchallMiddlewareRequest( |
| | req, |
| | res, |
| | parsedUrl |
| | ) |
| | if (finished) return |
| |
|
| | const err = new Error() |
| | ;(err as any).result = { |
| | response: new Response(null, { |
| | headers: { |
| | 'x-middleware-next': '1', |
| | }, |
| | }), |
| | } |
| | ;(err as any).bubble = true |
| | throw err |
| | } |
| |
|
| | |
| | |
| |
|
| | |
| | if (!useMatchedPathHeader && pathnameInfo.basePath) { |
| | parsedUrl.pathname = removePathPrefix( |
| | parsedUrl.pathname, |
| | pathnameInfo.basePath |
| | ) |
| | } |
| |
|
| | res.statusCode = 200 |
| | return await this.run(req, res, parsedUrl) |
| | } catch (err: any) { |
| | if (err instanceof NoFallbackError) { |
| | throw err |
| | } |
| |
|
| | if ( |
| | (err && typeof err === 'object' && err.code === 'ERR_INVALID_URL') || |
| | err instanceof DecodeError || |
| | err instanceof NormalizeError |
| | ) { |
| | res.statusCode = 400 |
| | return this.renderError(null, req, res, '/_error', {}) |
| | } |
| |
|
| | if ( |
| | this.minimalMode || |
| | this.renderOpts.dev || |
| | (isBubbledError(err) && err.bubble) |
| | ) { |
| | throw err |
| | } |
| | this.logError(getProperError(err)) |
| | res.statusCode = 500 |
| | res.body('Internal Server Error').send() |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | private normalize = (pathname: string) => { |
| | const normalizers: Array<PathnameNormalizer> = [] |
| |
|
| | if (this.normalizers.data) { |
| | normalizers.push(this.normalizers.data) |
| | } |
| |
|
| | |
| | |
| | if (this.normalizers.segmentPrefetchRSC) { |
| | normalizers.push(this.normalizers.segmentPrefetchRSC) |
| | } |
| |
|
| | if (this.normalizers.rsc) { |
| | normalizers.push(this.normalizers.rsc) |
| | } |
| |
|
| | for (const normalizer of normalizers) { |
| | if (!normalizer.match(pathname)) continue |
| |
|
| | return normalizer.normalize(pathname, true) |
| | } |
| |
|
| | return pathname |
| | } |
| |
|
| | private normalizeAndAttachMetadata: RouteHandler< |
| | ServerRequest, |
| | ServerResponse |
| | > = async (req, res, url) => { |
| | let finished = await this.handleNextImageRequest(req, res, url) |
| | if (finished) return true |
| |
|
| | if (this.enabledDirectories.pages) { |
| | finished = await this.handleNextDataRequest(req, res, url) |
| | if (finished) return true |
| | } |
| |
|
| | return false |
| | } |
| |
|
| | |
| | |
| | |
| | public getRequestHandlerWithMetadata( |
| | meta: RequestMeta |
| | ): BaseRequestHandler<ServerRequest, ServerResponse> { |
| | const handler = this.getRequestHandler() |
| | return (req, res, parsedUrl) => { |
| | setRequestMeta(req, meta) |
| | return handler(req, res, parsedUrl) |
| | } |
| | } |
| |
|
| | public getRequestHandler(): BaseRequestHandler< |
| | ServerRequest, |
| | ServerResponse |
| | > { |
| | return this.handleRequest.bind(this) |
| | } |
| |
|
| | protected abstract handleUpgrade( |
| | req: ServerRequest, |
| | socket: any, |
| | head?: any |
| | ): Promise<void> |
| |
|
| | public setAssetPrefix(prefix?: string): void { |
| | this.nextConfig.assetPrefix = prefix ? prefix.replace(/\/$/, '') : '' |
| | } |
| |
|
| | protected prepared: boolean = false |
| | protected preparedPromise: Promise<void> | null = null |
| | |
| | |
| | |
| | |
| | public async prepare(): Promise<void> { |
| | if (this.prepared) return |
| |
|
| | |
| | if (!this.instrumentation) { |
| | this.instrumentation = await this.loadInstrumentationModule() |
| | } |
| | if (this.preparedPromise === null) { |
| | this.preparedPromise = this.prepareImpl().then(() => { |
| | this.prepared = true |
| | this.preparedPromise = null |
| | }) |
| | } |
| | return this.preparedPromise |
| | } |
| | protected async prepareImpl(): Promise<void> {} |
| | protected async loadInstrumentationModule(): Promise<any> {} |
| |
|
| | public async close(): Promise<void> {} |
| |
|
| | protected getAppPathRoutes(): Record<string, string[]> { |
| | const appPathRoutes: Record<string, string[]> = {} |
| |
|
| | Object.keys(this.appPathsManifest || {}).forEach((entry) => { |
| | const normalizedPath = normalizeAppPath(entry) |
| | if (!appPathRoutes[normalizedPath]) { |
| | appPathRoutes[normalizedPath] = [] |
| | } |
| | appPathRoutes[normalizedPath].push(entry) |
| | }) |
| | return appPathRoutes |
| | } |
| |
|
| | protected async run( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | parsedUrl: NextUrlWithParsedQuery |
| | ): Promise<void> { |
| | return getTracer().trace(BaseServerSpan.run, async () => |
| | this.runImpl(req, res, parsedUrl) |
| | ) |
| | } |
| |
|
| | private async runImpl( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | parsedUrl: NextUrlWithParsedQuery |
| | ): Promise<void> { |
| | await this.handleCatchallRenderRequest(req, res, parsedUrl) |
| | } |
| |
|
| | private async pipe( |
| | fn: ( |
| | ctx: RequestContext<ServerRequest, ServerResponse> |
| | ) => Promise<ResponsePayload | null>, |
| | partialContext: Omit< |
| | RequestContext<ServerRequest, ServerResponse>, |
| | 'renderOpts' |
| | > |
| | ): Promise<void> { |
| | return getTracer().trace(BaseServerSpan.pipe, async () => |
| | this.pipeImpl(fn, partialContext) |
| | ) |
| | } |
| |
|
| | private async pipeImpl( |
| | fn: ( |
| | ctx: RequestContext<ServerRequest, ServerResponse> |
| | ) => Promise<ResponsePayload | null>, |
| | partialContext: Omit< |
| | RequestContext<ServerRequest, ServerResponse>, |
| | 'renderOpts' |
| | > |
| | ): Promise<void> { |
| | const ua = partialContext.req.headers['user-agent'] || '' |
| |
|
| | const ctx: RequestContext<ServerRequest, ServerResponse> = { |
| | ...partialContext, |
| | renderOpts: { |
| | ...this.renderOpts, |
| | |
| | supportsDynamicResponse: !this.renderOpts.botType, |
| | serveStreamingMetadata: shouldServeStreamingMetadata( |
| | ua, |
| | this.nextConfig.htmlLimitedBots |
| | ), |
| | }, |
| | } |
| |
|
| | const payload = await fn(ctx) |
| | if (payload === null) { |
| | return |
| | } |
| | const { req, res } = ctx |
| | const originalStatus = res.statusCode |
| | const { body } = payload |
| | let { cacheControl } = payload |
| | if (!res.sent) { |
| | const { generateEtags, poweredByHeader, dev } = this.renderOpts |
| |
|
| | |
| | if (dev) { |
| | res.setHeader( |
| | 'Cache-Control', |
| | this.nextConfig.experimental.devCacheControlNoCache |
| | ? 'no-cache, must-revalidate' |
| | : 'no-store, must-revalidate' |
| | ) |
| | cacheControl = undefined |
| | } |
| |
|
| | if (cacheControl && cacheControl.expire === undefined) { |
| | cacheControl.expire = this.nextConfig.expireTime |
| | } |
| |
|
| | await this.sendRenderResult(req, res, { |
| | result: body, |
| | generateEtags, |
| | poweredByHeader, |
| | cacheControl, |
| | cdnCacheControlHeader: |
| | this.nextConfig.experimental.cdnCacheControlHeader, |
| | }) |
| | res.statusCode = originalStatus |
| | } |
| | } |
| |
|
| | private async getStaticHTML( |
| | fn: ( |
| | ctx: RequestContext<ServerRequest, ServerResponse> |
| | ) => Promise<ResponsePayload | null>, |
| | partialContext: Omit< |
| | RequestContext<ServerRequest, ServerResponse>, |
| | 'renderOpts' |
| | > |
| | ): Promise<string | null> { |
| | const ctx: RequestContext<ServerRequest, ServerResponse> = { |
| | ...partialContext, |
| | renderOpts: { |
| | ...this.renderOpts, |
| | supportsDynamicResponse: false, |
| | }, |
| | } |
| | const payload = await fn(ctx) |
| | if (payload === null) { |
| | return null |
| | } |
| | return payload.body.toUnchunkedString() |
| | } |
| |
|
| | public async render( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: NextParsedUrlQuery = {}, |
| | parsedUrl?: NextUrlWithParsedQuery, |
| | internalRender = false |
| | ): Promise<void> { |
| | return getTracer().trace(BaseServerSpan.render, async () => |
| | this.renderImpl(req, res, pathname, query, parsedUrl, internalRender) |
| | ) |
| | } |
| |
|
| | protected getWaitUntil(): WaitUntil | undefined { |
| | const builtinRequestContext = getBuiltinRequestContext() |
| | if (builtinRequestContext) { |
| | |
| | |
| | |
| |
|
| | |
| | return builtinRequestContext.waitUntil |
| | } |
| |
|
| | if (this.minimalMode) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | return undefined |
| | } |
| |
|
| | return this.getInternalWaitUntil() |
| | } |
| |
|
| | protected getInternalWaitUntil(): WaitUntil | undefined { |
| | return undefined |
| | } |
| |
|
| | private async renderImpl( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: NextParsedUrlQuery = {}, |
| | parsedUrl?: NextUrlWithParsedQuery, |
| | internalRender = false |
| | ): Promise<void> { |
| | if (!pathname.startsWith('/')) { |
| | console.warn( |
| | `Cannot render page with path "${pathname}", did you mean "/${pathname}"?. See more info here: https://nextjs.org/docs/messages/render-no-starting-slash` |
| | ) |
| | } |
| |
|
| | if ( |
| | this.serverOptions.customServer && |
| | pathname === '/index' && |
| | !(await this.hasPage('/index')) |
| | ) { |
| | |
| | |
| | pathname = '/' |
| | } |
| |
|
| | const ua = req.headers['user-agent'] || '' |
| | this.renderOpts.botType = getBotType(ua) |
| |
|
| | |
| | |
| | |
| | |
| | if ( |
| | !internalRender && |
| | !this.minimalMode && |
| | !getRequestMeta(req, 'isNextDataReq') && |
| | (req.url?.match(/^\/_next\//) || |
| | (this.hasStaticDir && req.url!.match(/^\/static\//))) |
| | ) { |
| | return this.handleRequest(req, res, parsedUrl) |
| | } |
| |
|
| | if (isBlockedPage(pathname)) { |
| | return this.render404(req, res, parsedUrl) |
| | } |
| |
|
| | return this.pipe((ctx) => this.renderToResponse(ctx), { |
| | req, |
| | res, |
| | pathname, |
| | query, |
| | }) |
| | } |
| |
|
| | protected async getStaticPaths({ |
| | pathname, |
| | }: { |
| | pathname: string |
| | urlPathname: string |
| | requestHeaders: import('./lib/incremental-cache').IncrementalCache['requestHeaders'] |
| | page: string |
| | isAppPath: boolean |
| | }): Promise<{ |
| | staticPaths?: string[] |
| | prerenderedRoutes?: PrerenderedRoute[] |
| | fallbackMode?: FallbackMode |
| | }> { |
| | |
| | const fallbackField = |
| | this.getPrerenderManifest().dynamicRoutes[pathname]?.fallback |
| |
|
| | return { |
| | |
| | |
| | staticPaths: undefined, |
| | fallbackMode: parseFallbackField(fallbackField), |
| | } |
| | } |
| |
|
| | private async renderToResponseWithComponents( |
| | requestContext: RequestContext<ServerRequest, ServerResponse>, |
| | findComponentsResult: FindComponentsResult |
| | ): Promise<ResponsePayload | null> { |
| | return getTracer().trace( |
| | BaseServerSpan.renderToResponseWithComponents, |
| | async () => |
| | this.renderToResponseWithComponentsImpl( |
| | requestContext, |
| | findComponentsResult |
| | ) |
| | ) |
| | } |
| |
|
| | protected pathCouldBeIntercepted(resolvedPathname: string): boolean { |
| | return ( |
| | isInterceptionRouteAppPath(resolvedPathname) || |
| | this.interceptionRoutePatterns.some((regexp) => { |
| | return regexp.test(resolvedPathname) |
| | }) |
| | ) |
| | } |
| |
|
| | protected setVaryHeader( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | isAppPath: boolean, |
| | resolvedPathname: string |
| | ): void { |
| | const baseVaryHeader = `${RSC_HEADER}, ${NEXT_ROUTER_STATE_TREE_HEADER}, ${NEXT_ROUTER_PREFETCH_HEADER}, ${NEXT_ROUTER_SEGMENT_PREFETCH_HEADER}` |
| | const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false |
| |
|
| | let addedNextUrlToVary = false |
| |
|
| | if (isAppPath && this.pathCouldBeIntercepted(resolvedPathname)) { |
| | |
| | |
| | res.appendHeader('vary', `${baseVaryHeader}, ${NEXT_URL}`) |
| | addedNextUrlToVary = true |
| | } else if (isAppPath || isRSCRequest) { |
| | |
| | |
| | res.appendHeader('vary', baseVaryHeader) |
| | } |
| |
|
| | if (!addedNextUrlToVary) { |
| | |
| | |
| | delete req.headers[NEXT_URL] |
| | } |
| | } |
| |
|
| | private async renderToResponseWithComponentsImpl( |
| | { |
| | req, |
| | res, |
| | pathname, |
| | renderOpts: opts, |
| | }: RequestContext<ServerRequest, ServerResponse>, |
| | { components, query }: FindComponentsResult |
| | ): Promise<ResponsePayload | null> { |
| | if (pathname === UNDERSCORE_NOT_FOUND_ROUTE) { |
| | pathname = '/404' |
| | } |
| | const isErrorPathname = pathname === '/_error' |
| | const is404Page = |
| | pathname === '/404' || (isErrorPathname && res.statusCode === 404) |
| | const is500Page = |
| | pathname === '/500' || (isErrorPathname && res.statusCode === 500) |
| | const isAppPath = components.isAppPath === true |
| |
|
| | const hasServerProps = !!components.getServerSideProps |
| | const isPossibleServerAction = getIsPossibleServerAction(req) |
| | let isSSG = !!components.getStaticProps |
| | |
| | const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | if ( |
| | !this.minimalMode && |
| | this.nextConfig.experimental.validateRSCRequestHeaders && |
| | isRSCRequest && |
| | |
| | |
| | |
| | !is404Page |
| | ) { |
| | const headers = req.headers |
| |
|
| | const prefetchHeaderValue = headers[NEXT_ROUTER_PREFETCH_HEADER] |
| | const routerPrefetch = |
| | prefetchHeaderValue !== undefined |
| | ? |
| | prefetchHeaderValue === '1' || prefetchHeaderValue === '2' |
| | ? prefetchHeaderValue |
| | : undefined |
| | : |
| | |
| | |
| | getRequestMeta(req, 'isPrefetchRSCRequest') |
| | ? '1' |
| | : undefined |
| |
|
| | const segmentPrefetchRSCRequest = |
| | headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] || |
| | getRequestMeta(req, 'segmentPrefetchRSCRequest') |
| |
|
| | const expectedHash = computeCacheBustingSearchParam( |
| | routerPrefetch, |
| | segmentPrefetchRSCRequest, |
| | headers[NEXT_ROUTER_STATE_TREE_HEADER], |
| | headers[NEXT_URL] |
| | ) |
| | const actualHash = |
| | getRequestMeta(req, 'cacheBustingSearchParam') ?? |
| | new URL(req.url || '', 'http://localhost').searchParams.get( |
| | NEXT_RSC_UNION_QUERY |
| | ) |
| |
|
| | if (expectedHash !== actualHash) { |
| | |
| | |
| | |
| | |
| | |
| | const url = new URL(req.url || '', 'http://localhost') |
| | setCacheBustingSearchParamWithHash(url, expectedHash) |
| | res.statusCode = 307 |
| | res.setHeader('location', `${url.pathname}${url.search}`) |
| | res.body('').send() |
| | return null |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | let urlPathname = parseUrl(req.url || '').pathname || '/' |
| |
|
| | let resolvedUrlPathname = getRequestMeta(req, 'rewroteURL') || urlPathname |
| |
|
| | this.setVaryHeader(req, res, isAppPath, resolvedUrlPathname) |
| |
|
| | let staticPaths: string[] | undefined |
| | let hasFallback = false |
| |
|
| | const prerenderManifest = this.getPrerenderManifest() |
| |
|
| | if ( |
| | hasFallback || |
| | staticPaths?.includes(resolvedUrlPathname) || |
| | |
| | |
| | req.headers['x-now-route-matches'] |
| | ) { |
| | isSSG = true |
| | } else if (!this.renderOpts.dev) { |
| | isSSG ||= !!prerenderManifest.routes[toRoute(pathname)] |
| | } |
| |
|
| | |
| | const isNextDataRequest = |
| | !!( |
| | getRequestMeta(req, 'isNextDataReq') || |
| | (req.headers['x-nextjs-data'] && |
| | (this.serverOptions as any).webServerConfig) |
| | ) && |
| | (isSSG || hasServerProps) |
| |
|
| | |
| | |
| | |
| | if ( |
| | !isSSG && |
| | req.headers['x-middleware-prefetch'] && |
| | !(is404Page || pathname === '/_error') |
| | ) { |
| | res.setHeader(MATCHED_PATH_HEADER, pathname) |
| | res.setHeader('x-middleware-skip', '1') |
| | res.setHeader( |
| | 'cache-control', |
| | 'private, no-cache, no-store, max-age=0, must-revalidate' |
| | ) |
| | res.body('{}').send() |
| | return null |
| | } |
| |
|
| | |
| | |
| | if ( |
| | isSSG && |
| | this.minimalMode && |
| | req.headers[MATCHED_PATH_HEADER] && |
| | req.url.startsWith('/_next/data') |
| | ) { |
| | req.url = this.stripNextDataPath(req.url) |
| | } |
| |
|
| | const locale = getRequestMeta(req, 'locale') |
| |
|
| | if ( |
| | !!req.headers['x-nextjs-data'] && |
| | (!res.statusCode || res.statusCode === 200) |
| | ) { |
| | res.setHeader( |
| | 'x-nextjs-matched-path', |
| | `${locale ? `/${locale}` : ''}${pathname}` |
| | ) |
| | } |
| |
|
| | let routeModule: RouteModule | undefined |
| | if (components.routeModule) { |
| | routeModule = components.routeModule |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | const couldSupportPPR: boolean = |
| | this.isAppPPREnabled && |
| | typeof routeModule !== 'undefined' && |
| | isAppPageRouteModule(routeModule) |
| |
|
| | |
| | |
| | const hasDebugStaticShellQuery = |
| | process.env.__NEXT_EXPERIMENTAL_STATIC_SHELL_DEBUGGING === '1' && |
| | typeof query.__nextppronly !== 'undefined' && |
| | couldSupportPPR |
| |
|
| | |
| | |
| | const isRoutePPREnabled: boolean = |
| | couldSupportPPR && |
| | (( |
| | prerenderManifest.routes[pathname] ?? |
| | prerenderManifest.dynamicRoutes[pathname] |
| | )?.renderingMode === 'PARTIALLY_STATIC' || |
| | |
| | |
| | |
| | |
| | (hasDebugStaticShellQuery && |
| | (this.renderOpts.dev === true || |
| | this.experimentalTestProxy === true))) |
| |
|
| | |
| | |
| | |
| | const minimalPostponed = isRoutePPREnabled |
| | ? getRequestMeta(req, 'postponed') |
| | : undefined |
| |
|
| | |
| | if (is404Page && !isNextDataRequest && !isRSCRequest) { |
| | res.statusCode = 404 |
| | } |
| |
|
| | |
| | |
| | if (STATIC_STATUS_PAGES.includes(pathname)) { |
| | res.statusCode = parseInt(pathname.slice(1), 10) |
| | } |
| |
|
| | if ( |
| | |
| | !isPossibleServerAction && |
| | |
| | !minimalPostponed && |
| | !is404Page && |
| | !is500Page && |
| | pathname !== '/_error' && |
| | req.method !== 'HEAD' && |
| | req.method !== 'GET' && |
| | (typeof components.Component === 'string' || isSSG) |
| | ) { |
| | res.statusCode = 405 |
| | res.setHeader('Allow', ['GET', 'HEAD']) |
| | res.body('Method Not Allowed').send() |
| | return null |
| | } |
| |
|
| | |
| | if (typeof components.Component === 'string') { |
| | return { |
| | body: RenderResult.fromStatic( |
| | components.Component, |
| | HTML_CONTENT_TYPE_HEADER |
| | ), |
| | } |
| | } |
| |
|
| | if (opts.supportsDynamicResponse === true) { |
| | const ua = req.headers['user-agent'] || '' |
| | const isBotRequest = isBot(ua) |
| | const isSupportedDocument = |
| | typeof components.Document?.getInitialProps !== 'function' || |
| | |
| | NEXT_BUILTIN_DOCUMENT in components.Document |
| |
|
| | |
| | |
| | |
| | |
| | |
| | opts.supportsDynamicResponse = |
| | !isSSG && !isBotRequest && isSupportedDocument |
| | } |
| |
|
| | |
| | if (!isNextDataRequest && isAppPath && opts.dev) { |
| | opts.supportsDynamicResponse = true |
| | } |
| |
|
| | if (isSSG && this.minimalMode && req.headers[MATCHED_PATH_HEADER]) { |
| | |
| | resolvedUrlPathname = urlPathname |
| | } |
| |
|
| | urlPathname = removeTrailingSlash(urlPathname) |
| | resolvedUrlPathname = removeTrailingSlash(resolvedUrlPathname) |
| | if (this.localeNormalizer) { |
| | resolvedUrlPathname = this.localeNormalizer.normalize(resolvedUrlPathname) |
| | } |
| |
|
| | |
| | |
| | if (isNextDataRequest) { |
| | resolvedUrlPathname = this.stripNextDataPath(resolvedUrlPathname) |
| | urlPathname = this.stripNextDataPath(urlPathname) |
| | } |
| |
|
| | |
| | const incrementalCache: import('./lib/incremental-cache').IncrementalCache = |
| | await this.getIncrementalCache({ |
| | requestHeaders: Object.assign({}, req.headers), |
| | }) |
| |
|
| | |
| | incrementalCache.resetRequestCache() |
| |
|
| | if ( |
| | routeModule?.isDev && |
| | isDynamicRoute(pathname) && |
| | (components.getStaticPaths || isAppPath) |
| | ) { |
| | let getStaticPathsStart: bigint | undefined |
| | if (opts.dev) { |
| | getStaticPathsStart = process.hrtime.bigint() |
| | } |
| |
|
| | const pathsResults = await this.getStaticPaths({ |
| | pathname, |
| | urlPathname, |
| | requestHeaders: req.headers, |
| | page: components.page, |
| | isAppPath, |
| | }) |
| |
|
| | if (opts.dev && getStaticPathsStart && pathsResults.staticPaths?.length) { |
| | addRequestMeta( |
| | req, |
| | 'devGenerateStaticParamsDuration', |
| | process.hrtime.bigint() - getStaticPathsStart |
| | ) |
| | } |
| |
|
| | if (isAppPath && this.nextConfig.cacheComponents) { |
| | if (pathsResults.prerenderedRoutes?.length) { |
| | let smallestFallbackRouteParams = null |
| | for (const route of pathsResults.prerenderedRoutes) { |
| | const fallbackRouteParams = route.fallbackRouteParams |
| | if (!fallbackRouteParams || fallbackRouteParams.length === 0) { |
| | |
| | smallestFallbackRouteParams = null |
| | break |
| | } |
| | if ( |
| | smallestFallbackRouteParams === null || |
| | fallbackRouteParams.length < smallestFallbackRouteParams.length |
| | ) { |
| | smallestFallbackRouteParams = fallbackRouteParams |
| | } |
| | } |
| | if (smallestFallbackRouteParams) { |
| | addRequestMeta( |
| | req, |
| | 'devFallbackParams', |
| | createOpaqueFallbackRouteParams(smallestFallbackRouteParams)! |
| | ) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | |
| | if ( |
| | req.method === 'OPTIONS' && |
| | !is404Page && |
| | (!routeModule || !isAppRouteRouteModule(routeModule)) |
| | ) { |
| | await sendResponse(req, res, new Response(null, { status: 400 })) |
| | return null |
| | } |
| |
|
| | const request = isNodeNextRequest(req) ? req.originalRequest : req |
| | const response = isNodeNextResponse(res) ? res.originalResponse : res |
| |
|
| | const parsedInitUrl = parseUrl(getRequestMeta(req, 'initURL') || req.url) |
| | let initPathname = parsedInitUrl.pathname || '/' |
| |
|
| | for (const normalizer of [ |
| | this.normalizers.segmentPrefetchRSC, |
| | this.normalizers.rsc, |
| | ]) { |
| | if (normalizer?.match(initPathname)) { |
| | initPathname = normalizer.normalize(initPathname) |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | if (!(this.minimalMode && isErrorPathname)) { |
| | request.url = `${initPathname}${parsedInitUrl.search || ''}` |
| | } |
| |
|
| | |
| | setRequestMeta(request, getRequestMeta(req)) |
| | addRequestMeta(request, 'distDir', this.distDir) |
| | addRequestMeta(request, 'query', query) |
| | addRequestMeta(request, 'params', opts.params) |
| | addRequestMeta(request, 'minimalMode', this.minimalMode) |
| |
|
| | if (opts.err) { |
| | addRequestMeta(request, 'invokeError', opts.err) |
| | } |
| |
|
| | const maybeDevRequest: ServerRequest | IncomingMessage = |
| | |
| | |
| | |
| | |
| | process.env.NODE_ENV === 'development' |
| | ? new Proxy(request, { |
| | get(target: any, prop) { |
| | if (typeof target[prop] === 'function') { |
| | return target[prop].bind(target) |
| | } |
| | return target[prop] |
| | }, |
| | set(target: any, prop, value) { |
| | if (prop === 'fetchMetrics') { |
| | ;(req as any).fetchMetrics = value |
| | } |
| | target[prop] = value |
| | return true |
| | }, |
| | }) |
| | : request |
| |
|
| | |
| | |
| | let handlerReq: IncomingMessage = maybeDevRequest |
| | |
| | |
| | let handlerRes: HTTPServerResponse = response |
| |
|
| | await components.ComponentMod.handler(handlerReq, handlerRes, { |
| | waitUntil: this.getWaitUntil(), |
| | }) |
| |
|
| | |
| | return null |
| | } |
| |
|
| | private stripNextDataPath(filePath: string, stripLocale = true) { |
| | if (filePath.includes(this.buildId)) { |
| | const splitPath = filePath.substring( |
| | filePath.indexOf(this.buildId) + this.buildId.length |
| | ) |
| |
|
| | filePath = denormalizePagePath(splitPath.replace(/\.json$/, '')) |
| | } |
| |
|
| | if (this.localeNormalizer && stripLocale) { |
| | return this.localeNormalizer.normalize(filePath) |
| | } |
| | return filePath |
| | } |
| |
|
| | |
| | protected getOriginalAppPaths(route: string) { |
| | if (this.enabledDirectories.app) { |
| | const originalAppPath = this.appPathRoutes?.[route] |
| |
|
| | if (!originalAppPath) { |
| | return null |
| | } |
| |
|
| | return originalAppPath |
| | } |
| | return null |
| | } |
| |
|
| | protected async renderPageComponent( |
| | ctx: RequestContext<ServerRequest, ServerResponse>, |
| | bubbleNoFallback: boolean |
| | ) { |
| | const { query, pathname } = ctx |
| |
|
| | const appPaths = this.getOriginalAppPaths(pathname) |
| | const isAppPath = Array.isArray(appPaths) |
| |
|
| | let page = pathname |
| | if (isAppPath) { |
| | |
| | page = appPaths[appPaths.length - 1] |
| | } |
| |
|
| | const result = await this.findPageComponents({ |
| | locale: getRequestMeta(ctx.req, 'locale'), |
| | page, |
| | query, |
| | params: ctx.renderOpts.params || {}, |
| | isAppPath, |
| | sriEnabled: !!this.nextConfig.experimental.sri?.algorithm, |
| | appPaths, |
| | |
| | shouldEnsure: false, |
| | }) |
| | if (result) { |
| | getTracer().setRootSpanAttribute('next.route', pathname) |
| | try { |
| | return await this.renderToResponseWithComponents(ctx, result) |
| | } catch (err) { |
| | const isNoFallbackError = err instanceof NoFallbackError |
| |
|
| | if (!isNoFallbackError || (isNoFallbackError && bubbleNoFallback)) { |
| | throw err |
| | } |
| | } |
| | } |
| | return false |
| | } |
| |
|
| | private async renderToResponse( |
| | ctx: RequestContext<ServerRequest, ServerResponse> |
| | ): Promise<ResponsePayload | null> { |
| | return getTracer().trace( |
| | BaseServerSpan.renderToResponse, |
| | { |
| | spanName: `rendering page`, |
| | attributes: { |
| | 'next.route': ctx.pathname, |
| | }, |
| | }, |
| | async () => { |
| | return this.renderToResponseImpl(ctx) |
| | } |
| | ) |
| | } |
| |
|
| | protected abstract getMiddleware(): Promise<MiddlewareRoutingItem | undefined> |
| | protected abstract getFallbackErrorComponents( |
| | url?: string |
| | ): Promise<LoadComponentsReturnType<ErrorModule> | null> |
| | protected abstract getRoutesManifest(): NormalizedRouteManifest | undefined |
| |
|
| | private async renderToResponseImpl( |
| | ctx: RequestContext<ServerRequest, ServerResponse> |
| | ): Promise<ResponsePayload | null> { |
| | const { req, res, query, pathname } = ctx |
| | let page = pathname |
| | const bubbleNoFallback = |
| | getRequestMeta(ctx.req, 'bubbleNoFallback') ?? false |
| |
|
| | if ( |
| | !this.minimalMode && |
| | this.nextConfig.experimental.validateRSCRequestHeaders |
| | ) { |
| | addRequestMeta( |
| | ctx.req, |
| | 'cacheBustingSearchParam', |
| | query[NEXT_RSC_UNION_QUERY] |
| | ) |
| | } |
| | delete query[NEXT_RSC_UNION_QUERY] |
| |
|
| | const options: MatchOptions = { |
| | i18n: this.i18nProvider?.fromRequest(req, pathname), |
| | } |
| |
|
| | const existingMatch = getRequestMeta(ctx.req, 'match') |
| |
|
| | let fastPath = true |
| | |
| | |
| | const invokeOutput = getRequestMeta(ctx.req, 'invokeOutput') |
| |
|
| | if ( |
| | (!this.minimalMode && |
| | typeof invokeOutput === 'string' && |
| | isDynamicRoute(invokeOutput || '') && |
| | invokeOutput !== existingMatch?.definition.pathname) || |
| | |
| | |
| | |
| | existingMatch?.definition.page.includes('/@') |
| | ) { |
| | fastPath = false |
| | } |
| |
|
| | try { |
| | for await (const match of fastPath && existingMatch |
| | ? [existingMatch] |
| | : this.matchers.matchAll(pathname, options)) { |
| | if ( |
| | !this.minimalMode && |
| | typeof invokeOutput === 'string' && |
| | isDynamicRoute(invokeOutput || '') && |
| | invokeOutput !== match.definition.pathname |
| | ) { |
| | continue |
| | } |
| |
|
| | const result = await this.renderPageComponent( |
| | { |
| | ...ctx, |
| | pathname: match.definition.pathname, |
| | renderOpts: { |
| | ...ctx.renderOpts, |
| | params: match.params, |
| | }, |
| | }, |
| | bubbleNoFallback |
| | ) |
| | if (result !== false) return result |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if (this.serverOptions.webServerConfig) { |
| | |
| | ctx.pathname = this.serverOptions.webServerConfig.page |
| | const result = await this.renderPageComponent(ctx, bubbleNoFallback) |
| | if (result !== false) return result |
| | } |
| | } catch (error) { |
| | const err = getProperError(error) |
| |
|
| | if (error instanceof MissingStaticPage) { |
| | console.error( |
| | 'Invariant: failed to load static page', |
| | JSON.stringify( |
| | { |
| | page, |
| | url: ctx.req.url, |
| | matchedPath: ctx.req.headers[MATCHED_PATH_HEADER], |
| | initUrl: getRequestMeta(ctx.req, 'initURL'), |
| | didRewrite: !!getRequestMeta(ctx.req, 'rewroteURL'), |
| | rewroteUrl: getRequestMeta(ctx.req, 'rewroteURL'), |
| | }, |
| | null, |
| | 2 |
| | ) |
| | ) |
| | throw err |
| | } |
| |
|
| | if (err instanceof NoFallbackError && bubbleNoFallback) { |
| | throw err |
| | } |
| | if (err instanceof DecodeError || err instanceof NormalizeError) { |
| | res.statusCode = 400 |
| | return await this.renderErrorToResponse(ctx, err) |
| | } |
| |
|
| | res.statusCode = 500 |
| |
|
| | |
| | |
| | if (await this.hasPage('/500')) { |
| | addRequestMeta(ctx.req, 'customErrorRender', true) |
| | await this.renderErrorToResponse(ctx, err) |
| | removeRequestMeta(ctx.req, 'customErrorRender') |
| | } |
| |
|
| | const isWrappedError = err instanceof WrappedBuildError |
| |
|
| | if (!isWrappedError) { |
| | if (this.minimalMode || this.renderOpts.dev) { |
| | if (isError(err)) err.page = page |
| | throw err |
| | } |
| | this.logError(getProperError(err)) |
| | } |
| | const response = await this.renderErrorToResponse( |
| | ctx, |
| | isWrappedError ? (err as WrappedBuildError).innerError : err |
| | ) |
| | return response |
| | } |
| |
|
| | const middleware = await this.getMiddleware() |
| | if ( |
| | middleware && |
| | !!ctx.req.headers['x-nextjs-data'] && |
| | (!res.statusCode || res.statusCode === 200 || res.statusCode === 404) |
| | ) { |
| | const locale = getRequestMeta(req, 'locale') |
| |
|
| | res.setHeader( |
| | 'x-nextjs-matched-path', |
| | `${locale ? `/${locale}` : ''}${pathname}` |
| | ) |
| | res.statusCode = 200 |
| | res.setHeader('Content-Type', JSON_CONTENT_TYPE_HEADER) |
| | res.body('{}') |
| | res.send() |
| | return null |
| | } |
| |
|
| | res.statusCode = 404 |
| | return this.renderErrorToResponse(ctx, null) |
| | } |
| |
|
| | public async renderToHTML( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: ParsedUrlQuery = {} |
| | ): Promise<string | null> { |
| | return getTracer().trace(BaseServerSpan.renderToHTML, async () => { |
| | return this.renderToHTMLImpl(req, res, pathname, query) |
| | }) |
| | } |
| |
|
| | private async renderToHTMLImpl( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: ParsedUrlQuery = {} |
| | ): Promise<string | null> { |
| | return this.getStaticHTML((ctx) => this.renderToResponse(ctx), { |
| | req, |
| | res, |
| | pathname, |
| | query, |
| | }) |
| | } |
| |
|
| | public async renderError( |
| | err: Error | null, |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: NextParsedUrlQuery = {}, |
| | setHeaders = true |
| | ): Promise<void> { |
| | return getTracer().trace(BaseServerSpan.renderError, async () => { |
| | return this.renderErrorImpl(err, req, res, pathname, query, setHeaders) |
| | }) |
| | } |
| |
|
| | private async renderErrorImpl( |
| | err: Error | null, |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: NextParsedUrlQuery = {}, |
| | setHeaders = true |
| | ): Promise<void> { |
| | if (setHeaders) { |
| | res.setHeader( |
| | 'Cache-Control', |
| | 'private, no-cache, no-store, max-age=0, must-revalidate' |
| | ) |
| | } |
| |
|
| | return this.pipe( |
| | async (ctx) => { |
| | const response = await this.renderErrorToResponse(ctx, err) |
| | if (this.minimalMode && res.statusCode === 500) { |
| | throw err |
| | } |
| | return response |
| | }, |
| | { req, res, pathname, query } |
| | ) |
| | } |
| |
|
| | private customErrorNo404Warn = execOnce(() => { |
| | Log.warn( |
| | `You have added a custom /_error page without a custom /404 page. This prevents the 404 page from being auto statically optimized.\nSee here for info: https://nextjs.org/docs/messages/custom-error-no-custom-404` |
| | ) |
| | }) |
| |
|
| | private async renderErrorToResponse( |
| | ctx: RequestContext<ServerRequest, ServerResponse>, |
| | err: Error | null |
| | ): Promise<ResponsePayload | null> { |
| | return getTracer().trace(BaseServerSpan.renderErrorToResponse, async () => { |
| | return this.renderErrorToResponseImpl(ctx, err) |
| | }) |
| | } |
| |
|
| | protected async renderErrorToResponseImpl( |
| | ctx: RequestContext<ServerRequest, ServerResponse>, |
| | err: Error | null |
| | ): Promise<ResponsePayload | null> { |
| | |
| | |
| | if (this.renderOpts.dev && ctx.pathname === '/favicon.ico') { |
| | return { |
| | body: RenderResult.EMPTY, |
| | } |
| | } |
| | const { res, query } = ctx |
| |
|
| | try { |
| | let result: null | FindComponentsResult = null |
| |
|
| | const is404 = res.statusCode === 404 |
| | let using404Page = false |
| | const hasAppDir = this.enabledDirectories.app |
| |
|
| | if (is404) { |
| | if (hasAppDir) { |
| | |
| | result = await this.findPageComponents({ |
| | locale: getRequestMeta(ctx.req, 'locale'), |
| | page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, |
| | query, |
| | params: {}, |
| | isAppPath: true, |
| | shouldEnsure: true, |
| | url: ctx.req.url, |
| | }) |
| | using404Page = result !== null |
| | } |
| |
|
| | if (!result && (await this.hasPage('/404'))) { |
| | result = await this.findPageComponents({ |
| | locale: getRequestMeta(ctx.req, 'locale'), |
| | page: '/404', |
| | query, |
| | params: {}, |
| | isAppPath: false, |
| | |
| | shouldEnsure: true, |
| | url: ctx.req.url, |
| | }) |
| | using404Page = result !== null |
| | } |
| | } |
| | let statusPage = `/${res.statusCode}` |
| |
|
| | if ( |
| | !getRequestMeta(ctx.req, 'customErrorRender') && |
| | !result && |
| | STATIC_STATUS_PAGES.includes(statusPage) |
| | ) { |
| | |
| | |
| | if (statusPage !== '/500' || !this.renderOpts.dev) { |
| | if (!result && hasAppDir) { |
| | |
| | result = await this.findPageComponents({ |
| | locale: getRequestMeta(ctx.req, 'locale'), |
| | page: statusPage, |
| | query, |
| | params: {}, |
| | isAppPath: true, |
| | |
| | |
| | shouldEnsure: true, |
| | url: ctx.req.url, |
| | }) |
| | } |
| | |
| | result = await this.findPageComponents({ |
| | locale: getRequestMeta(ctx.req, 'locale'), |
| | page: statusPage, |
| | query, |
| | params: {}, |
| | isAppPath: false, |
| | |
| | |
| | shouldEnsure: true, |
| | url: ctx.req.url, |
| | }) |
| | } |
| | } |
| |
|
| | if (!result) { |
| | result = await this.findPageComponents({ |
| | locale: getRequestMeta(ctx.req, 'locale'), |
| | page: '/_error', |
| | query, |
| | params: {}, |
| | isAppPath: false, |
| | |
| | |
| | shouldEnsure: true, |
| | url: ctx.req.url, |
| | }) |
| | statusPage = '/_error' |
| | } |
| |
|
| | if ( |
| | process.env.NODE_ENV !== 'production' && |
| | !using404Page && |
| | (await this.hasPage('/_error')) && |
| | !(await this.hasPage('/404')) |
| | ) { |
| | this.customErrorNo404Warn() |
| | } |
| |
|
| | if (!result) { |
| | |
| | |
| | if (this.renderOpts.dev) { |
| | return { |
| | |
| | body: RenderResult.fromStatic( |
| | ` |
| | <pre>missing required error components, refreshing...</pre> |
| | <script> |
| | async function check() { |
| | const res = await fetch(location.href).catch(() => ({})) |
| | |
| | if (res.status === 200) { |
| | location.reload() |
| | } else { |
| | setTimeout(check, 1000) |
| | } |
| | } |
| | check() |
| | </script>`, |
| | HTML_CONTENT_TYPE_HEADER |
| | ), |
| | } |
| | } |
| |
|
| | throw new WrappedBuildError( |
| | new Error('missing required error components') |
| | ) |
| | } |
| |
|
| | |
| | |
| | if (result.components.routeModule) { |
| | addRequestMeta(ctx.req, 'match', { |
| | definition: result.components.routeModule.definition, |
| | params: undefined, |
| | }) |
| | } else { |
| | removeRequestMeta(ctx.req, 'match') |
| | } |
| |
|
| | try { |
| | return await this.renderToResponseWithComponents( |
| | { |
| | ...ctx, |
| | pathname: statusPage, |
| | renderOpts: { |
| | ...ctx.renderOpts, |
| | err, |
| | }, |
| | }, |
| | result |
| | ) |
| | } catch (maybeFallbackError) { |
| | if (maybeFallbackError instanceof NoFallbackError) { |
| | throw new Error('invariant: failed to render error page') |
| | } |
| | throw maybeFallbackError |
| | } |
| | } catch (error) { |
| | const renderToHtmlError = getProperError(error) |
| | const isWrappedError = renderToHtmlError instanceof WrappedBuildError |
| | if (!isWrappedError) { |
| | this.logError(renderToHtmlError) |
| | } |
| | res.statusCode = 500 |
| | const fallbackComponents = await this.getFallbackErrorComponents( |
| | ctx.req.url |
| | ) |
| |
|
| | if (fallbackComponents) { |
| | |
| | |
| | addRequestMeta(ctx.req, 'match', { |
| | definition: fallbackComponents.routeModule!.definition, |
| | params: undefined, |
| | }) |
| |
|
| | return this.renderToResponseWithComponents( |
| | { |
| | ...ctx, |
| | pathname: '/_error', |
| | renderOpts: { |
| | ...ctx.renderOpts, |
| | |
| | |
| | err: isWrappedError |
| | ? renderToHtmlError.innerError |
| | : renderToHtmlError, |
| | }, |
| | }, |
| | { |
| | query, |
| | components: fallbackComponents, |
| | } |
| | ) |
| | } |
| | return { |
| | body: RenderResult.fromStatic('Internal Server Error', 'text/plain'), |
| | } |
| | } |
| | } |
| |
|
| | public async renderErrorToHTML( |
| | err: Error | null, |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | pathname: string, |
| | query: ParsedUrlQuery = {} |
| | ): Promise<string | null> { |
| | return this.getStaticHTML((ctx) => this.renderErrorToResponse(ctx, err), { |
| | req, |
| | res, |
| | pathname, |
| | query, |
| | }) |
| | } |
| |
|
| | public async render404( |
| | req: ServerRequest, |
| | res: ServerResponse, |
| | parsedUrl?: Pick<NextUrlWithParsedQuery, 'pathname' | 'query'>, |
| | setHeaders = true |
| | ): Promise<void> { |
| | const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(req.url) |
| |
|
| | |
| | if (this.nextConfig.i18n) { |
| | if (!getRequestMeta(req, 'locale')) { |
| | addRequestMeta(req, 'locale', this.nextConfig.i18n.defaultLocale) |
| | } |
| | addRequestMeta(req, 'defaultLocale', this.nextConfig.i18n.defaultLocale) |
| | } |
| |
|
| | res.statusCode = 404 |
| | return this.renderError(null, req, res, pathname!, query, setHeaders) |
| | } |
| | } |
| |
|