UNPKG

next

Version:

The React Framework

916 lines • 87.8 kB
import { NormalizeError, DecodeError, normalizeRepeatedSlashes, MissingStaticPage } from "../shared/lib/utils"; import { format as formatUrl, parse as parseUrl } from "url"; import { getRedirectStatus } from "../lib/redirect-status"; import { isEdgeRuntime } from "../lib/is-edge-runtime"; import { APP_PATHS_MANIFEST, NEXT_BUILTIN_DOCUMENT, PAGES_MANIFEST, STATIC_STATUS_PAGES, TEMPORARY_REDIRECT_STATUS } from "../shared/lib/constants"; import { isDynamicRoute } from "../shared/lib/router/utils"; import { setLazyProp, getCookieParser, checkIsOnDemandRevalidate } from "./api-utils"; import { setConfig } from "../shared/lib/runtime-config"; import { setRevalidateHeaders } from "./send-payload/revalidate-headers"; import { execOnce } from "../shared/lib/utils"; import { isBlockedPage } from "./utils"; import { 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 escapePathDelimiters from "../shared/lib/router/utils/escape-path-delimiters"; import { getUtils } from "./server-utils"; import isError, { getProperError } from "../lib/is-error"; import { addRequestMeta, getRequestMeta, removeRequestMeta } from "./request-meta"; import { removePathPrefix } from "../shared/lib/router/utils/remove-path-prefix"; import { normalizeAppPath, normalizeRscPath } 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"; import { RSC, RSC_VARY_HEADER, FLIGHT_PARAMETERS, NEXT_RSC_UNION_QUERY } from "../client/components/app-router-headers"; import { LocaleRouteNormalizer } from "./future/normalizers/locale-route-normalizer"; import { DefaultRouteMatcherManager } from "./future/route-matcher-managers/default-route-matcher-manager"; import { AppPageRouteMatcherProvider } from "./future/route-matcher-providers/app-page-route-matcher-provider"; import { AppRouteRouteMatcherProvider } from "./future/route-matcher-providers/app-route-route-matcher-provider"; import { PagesAPIRouteMatcherProvider } from "./future/route-matcher-providers/pages-api-route-matcher-provider"; import { PagesRouteMatcherProvider } from "./future/route-matcher-providers/pages-route-matcher-provider"; import { ServerManifestLoader } from "./future/route-matcher-providers/helpers/manifest-loaders/server-manifest-loader"; import { getTracer, SpanKind } from "./lib/trace/tracer"; import { BaseServerSpan } from "./lib/trace/constants"; import { I18NProvider } from "./future/helpers/i18n-provider"; import { sendResponse } from "./send-response"; import { RouteKind } from "./future/route-kind"; import { handleInternalServerErrorResponse } from "./future/route-modules/helpers/response-handlers"; import { fromNodeOutgoingHttpHeaders, toNodeOutgoingHttpHeaders } from "./web/utils"; import { NEXT_QUERY_PARAM_PREFIX } from "../lib/constants"; import { normalizeLocalePath } from "../shared/lib/i18n/normalize-locale-path"; import { NextRequestAdapter, signalFromNodeResponse } from "./web/spec-extension/adapters/next-request"; export class NoFallbackError extends Error { } // Internal wrapper around build errors at development // time, to prevent us from propagating or logging them export class WrappedBuildError extends Error { constructor(innerError){ super(); this.innerError = innerError; } } export default class Server { constructor(options){ var _this_nextConfig_i18n, _this_nextConfig_experimental_amp, _this_nextConfig_i18n1; this.prepared = false; this.preparedPromise = null; this.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`); }); const { dir ="." , quiet =false , conf , dev =false , minimalMode =false , customServer =true , hostname , port } = options; this.serverOptions = options; this.isRenderWorker = options._renderWorker; this.dir = process.env.NEXT_RUNTIME === "edge" ? dir : require("path").resolve(dir); this.quiet = quiet; this.loadEnvConfig({ dev }); // TODO: should conf be normalized to prevent missing // values from causing issues as this can be user provided this.nextConfig = conf; this.hostname = hostname; this.port = port; this.distDir = process.env.NEXT_RUNTIME === "edge" ? this.nextConfig.distDir : require("path").join(this.dir, this.nextConfig.distDir); this.publicDir = this.getPublicDir(); this.hasStaticDir = !minimalMode && this.getHasStaticDir(); this.i18nProvider = ((_this_nextConfig_i18n = this.nextConfig.i18n) == null ? void 0 : _this_nextConfig_i18n.locales) ? new I18NProvider(this.nextConfig.i18n) : undefined; // Configure the locale normalizer, it's used for routes inside `pages/`. this.localeNormalizer = this.i18nProvider ? new LocaleRouteNormalizer(this.i18nProvider) : undefined; // Only serverRuntimeConfig needs the default // publicRuntimeConfig gets it's default in client/index.js const { serverRuntimeConfig ={} , publicRuntimeConfig , assetPrefix , generateEtags } = this.nextConfig; this.buildId = this.getBuildId(); this.minimalMode = minimalMode || !!process.env.NEXT_PRIVATE_MINIMAL_MODE; this.hasAppDir = !!this.nextConfig.experimental.appDir && this.getHasAppDir(dev); const serverComponents = this.hasAppDir; this.nextFontManifest = this.getNextFontManifest(); if (process.env.NEXT_RUNTIME !== "edge") { if (this.nextConfig.experimental.deploymentId) { process.env.NEXT_DEPLOYMENT_ID = this.nextConfig.experimental.deploymentId; } } this.renderOpts = { deploymentId: this.nextConfig.experimental.deploymentId, strictNextHead: !!this.nextConfig.experimental.strictNextHead, poweredByHeader: this.nextConfig.poweredByHeader, canonicalBase: this.nextConfig.amp.canonicalBase || "", buildId: this.buildId, generateEtags, previewProps: this.getPrerenderManifest().preview, customServer: customServer === true ? true : undefined, ampOptimizerConfig: (_this_nextConfig_experimental_amp = this.nextConfig.experimental.amp) == null ? void 0 : _this_nextConfig_experimental_amp.optimizer, basePath: this.nextConfig.basePath, images: this.nextConfig.images, optimizeFonts: this.nextConfig.optimizeFonts, fontManifest: this.nextConfig.optimizeFonts && !dev ? this.getFontManifest() : undefined, optimizeCss: this.nextConfig.experimental.optimizeCss, nextConfigOutput: this.nextConfig.output, nextScriptWorkers: this.nextConfig.experimental.nextScriptWorkers, disableOptimizedLoading: this.nextConfig.experimental.disableOptimizedLoading, domainLocales: (_this_nextConfig_i18n1 = this.nextConfig.i18n) == null ? void 0 : _this_nextConfig_i18n1.domains, distDir: this.distDir, serverComponents, crossOrigin: this.nextConfig.crossOrigin ? this.nextConfig.crossOrigin : undefined, largePageDataBytes: this.nextConfig.experimental.largePageDataBytes, // Only the `publicRuntimeConfig` key is exposed to the client side // It'll be rendered as part of __NEXT_DATA__ on the client side runtimeConfig: Object.keys(publicRuntimeConfig).length > 0 ? publicRuntimeConfig : undefined }; // Initialize next/config with the environment configuration setConfig({ serverRuntimeConfig, publicRuntimeConfig }); this.pagesManifest = this.getPagesManifest(); this.appPathsManifest = this.getAppPathsManifest(); this.appPathRoutes = this.getAppPathRoutes(); // Configure the routes. const { matchers } = this.getRoutes(); this.matchers = matchers; // Start route compilation. We don't wait for the routes to finish loading // because we use the `waitTillReady` promise below in `handleRequest` to // wait. Also we can't `await` in the constructor. matchers.reload(); this.setAssetPrefix(assetPrefix); this.responseCache = this.getResponseCache({ dev }); } async normalizeNextData(_req, _res, _parsedUrl) { return { finished: false }; } async handleNextImageRequest(_req, _res, _parsedUrl) { return { finished: false }; } async handleCatchallRenderRequest(_req, _res, _parsedUrl) { return { finished: false }; } async handleCatchallMiddlewareRequest(_req, _res, _parsedUrl) { return { finished: false }; } getRoutes() { // Create a new manifest loader that get's the manifests from the server. 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; } }); // Configure the matchers and handlers. const matchers = new DefaultRouteMatcherManager(); // Match pages under `pages/`. matchers.push(new PagesRouteMatcherProvider(this.distDir, manifestLoader, this.i18nProvider)); // Match api routes under `pages/api/`. matchers.push(new PagesAPIRouteMatcherProvider(this.distDir, manifestLoader, this.i18nProvider)); // If the app directory is enabled, then add the app matchers and handlers. if (this.hasAppDir) { // Match app pages under `app/`. matchers.push(new AppPageRouteMatcherProvider(this.distDir, manifestLoader)); matchers.push(new AppRouteRouteMatcherProvider(this.distDir, manifestLoader)); } return { matchers }; } logError(err) { if (this.quiet) return; console.error(err); } async handleRequest(req, res, parsedUrl) { await this.prepare(); const method = req.method.toUpperCase(); return getTracer().trace(BaseServerSpan.handleRequest, { spanName: `${method} ${req.url}`, kind: SpanKind.SERVER, attributes: { "http.method": method, "http.target": req.url } }, async (span)=>this.handleRequestImpl(req, res, parsedUrl).finally(()=>{ if (!span) return; span.setAttributes({ "http.status_code": res.statusCode }); const rootSpanAttributes = getTracer().getRootSpanAttributes(); // We were unable to get attributes, probably OTEL is not enabled 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 newName = `${method} ${route}`; span.setAttributes({ "next.route": route, "http.route": route, "next.span_name": newName }); span.updateName(newName); } })); } async handleRequestImpl(req, res, parsedUrl) { try { var _this_i18nProvider, _this_nextConfig_i18n; // Wait for the matchers to be ready. await this.matchers.waitTillReady(); // ensure cookies set in middleware are merged and // not overridden by API routes/getServerSideProps const _res = res.originalResponse || res; const origSetHeader = _res.setHeader.bind(_res); _res.setHeader = (name, val)=>{ if (name.toLowerCase() === "set-cookie") { const middlewareValue = getRequestMeta(req, "_nextMiddlewareCookie"); if (!middlewareValue || !Array.isArray(val) || !val.every((item, idx)=>item === middlewareValue[idx])) { val = [ // TODO: (wyattjoh) find out why this is called multiple times resulting in duplicate cookies being added ...new Set([ ...middlewareValue || [], ...typeof val === "string" ? [ val ] : Array.isArray(val) ? val : [] ]) ]; } } return origSetHeader(name, val); }; const urlParts = (req.url || "").split("?"); const urlNoQuery = urlParts[0]; // this normalizes repeated slashes in the path e.g. hello//world -> // hello/world or backslashes to forward slashes, this does not // handle trailing slash as that is handled the same as a next.config.js // redirect if (urlNoQuery == null ? void 0 : urlNoQuery.match(/(\\|\/\/)/)) { const cleanUrl = normalizeRepeatedSlashes(req.url); res.redirect(cleanUrl, 308).body(cleanUrl).send(); return; } setLazyProp({ req: req }, "cookies", getCookieParser(req.headers)); // Parse url if parsedUrl not provided if (!parsedUrl || typeof parsedUrl !== "object") { parsedUrl = parseUrl(req.url, true); } // 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()]; } } } req.url = normalizeRscPath(req.url, this.hasAppDir); parsedUrl.pathname = normalizeRscPath(parsedUrl.pathname || "", this.hasAppDir); this.attachRequestMeta(req, parsedUrl); const domainLocale = (_this_i18nProvider = this.i18nProvider) == null ? void 0 : _this_i18nProvider.detectDomainLocale(getHostname(parsedUrl, req.headers)); const defaultLocale = (domainLocale == null ? void 0 : domainLocale.defaultLocale) || ((_this_nextConfig_i18n = this.nextConfig.i18n) == null ? void 0 : _this_nextConfig_i18n.defaultLocale); parsedUrl.query.__nextDefaultLocale = 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); addRequestMeta(req, "_nextHadBasePath", true); } const useMatchedPathHeader = this.minimalMode && typeof req.headers["x-matched-path"] === "string"; // TODO: merge handling with x-invoke-path if (useMatchedPathHeader) { try { var _this_i18nProvider1, _this_getRoutesManifest; if (this.hasAppDir) { // ensure /index path is normalized for prerender // in minimal mode if (req.url.match(/^\/index($|\?)/)) { req.url = req.url.replace(/^\/index/, "/"); } parsedUrl.pathname = parsedUrl.pathname === "/index" ? "/" : parsedUrl.pathname; } // 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"], "http://localhost").pathname, this.hasAppDir); let urlPathname = new URL(req.url, "http://localhost").pathname; // For ISR the URL is normalized to the prerenderPath so if // it's a data request the URL path will be the data URL, // basePath is already stripped by this point if (urlPathname.startsWith(`/_next/data/`)) { parsedUrl.query.__nextDataReq = "1"; } const normalizedUrlPath = this.stripNextDataPath(urlPathname); matchedPath = this.stripNextDataPath(matchedPath, false); // Perform locale detection and normalization. const localeAnalysisResult = (_this_i18nProvider1 = this.i18nProvider) == null ? void 0 : _this_i18nProvider1.analyze(matchedPath, { defaultLocale }); // The locale result will be defined even if the locale was not // detected for the request because it will be inferred from the // default locale. if (localeAnalysisResult) { parsedUrl.query.__nextLocale = localeAnalysisResult.detectedLocale; // If the detected locale was inferred from the default locale, we // need to modify the metadata on the request to indicate that. if (localeAnalysisResult.inferredFromDefault) { parsedUrl.query.__nextInferredLocaleFromDefault = "1"; } else { delete parsedUrl.query.__nextInferredLocaleFromDefault; } } // TODO: check if this is needed any more? matchedPath = denormalizePagePath(matchedPath); let srcPathname = matchedPath; const match = await this.matchers.match(matchedPath, { i18n: localeAnalysisResult }); // Update the source pathname to the matched page's pathname. if (match) srcPathname = match.definition.pathname; // The page is dynamic if the params are defined. const pageIsDynamic = typeof (match == null ? void 0 : match.params) !== "undefined"; // The rest of this function can't handle i18n properly, so ensure we // restore the pathname with the locale information stripped from it // now that we're done matching if we're using i18n. if (localeAnalysisResult) { matchedPath = localeAnalysisResult.pathname; } const utils = getUtils({ pageIsDynamic, page: srcPathname, i18n: this.nextConfig.i18n, basePath: this.nextConfig.basePath, rewrites: ((_this_getRoutesManifest = this.getRoutesManifest()) == null ? void 0 : _this_getRoutesManifest.rewrites) || { beforeFiles: [], afterFiles: [], fallback: [] }, caseSensitive: !!this.nextConfig.experimental.caseSensitiveRoutes }); // Ensure parsedUrl.pathname includes locale before processing // rewrites or they won't match correctly. if (defaultLocale && !pathnameInfo.locale) { parsedUrl.pathname = `/${defaultLocale}${parsedUrl.pathname}`; } const pathnameBeforeRewrite = parsedUrl.pathname; const rewriteParams = utils.handleRewrites(req, parsedUrl); const rewriteParamKeys = Object.keys(rewriteParams); const didRewrite = pathnameBeforeRewrite !== parsedUrl.pathname; if (didRewrite) { addRequestMeta(req, "_nextRewroteUrl", parsedUrl.pathname); addRequestMeta(req, "_nextDidRewrite", true); } const routeParamKeys = new Set(); for (const key of Object.keys(parsedUrl.query)){ const value = parsedUrl.query[key]; if (key !== NEXT_QUERY_PARAM_PREFIX && key.startsWith(NEXT_QUERY_PARAM_PREFIX)) { const normalizedKey = key.substring(NEXT_QUERY_PARAM_PREFIX.length); parsedUrl.query[normalizedKey] = value; routeParamKeys.add(normalizedKey); delete parsedUrl.query[key]; } } // interpolate dynamic params and normalize URL if needed if (pageIsDynamic) { let params = {}; let paramsResult = utils.normalizeDynamicRouteParams(parsedUrl.query); // for prerendered ISR paths we attempt parsing the route // params from the URL directly as route-matches may not // contain the correct values due to the filesystem path // matching before the dynamic route has been matched if (!paramsResult.hasValidParams && pageIsDynamic && !isDynamicRoute(normalizedUrlPath)) { let matcherParams = utils.dynamicRouteMatcher == null ? void 0 : utils.dynamicRouteMatcher(normalizedUrlPath); if (matcherParams) { utils.normalizeDynamicRouteParams(matcherParams); Object.assign(paramsResult.params, matcherParams); paramsResult.hasValidParams = true; } } if (paramsResult.hasValidParams) { params = paramsResult.params; } if (req.headers["x-now-route-matches"] && isDynamicRoute(matchedPath) && !paramsResult.hasValidParams) { const opts = {}; const routeParams = utils.getParamsFromRouteMatches(req, opts, parsedUrl.query.__nextLocale || ""); // If this returns a locale, it means that the locale was detected // from the pathname. if (opts.locale) { parsedUrl.query.__nextLocale = opts.locale; // As the locale was parsed from the pathname, we should mark // that the locale was not inferred as the default. delete parsedUrl.query.__nextInferredLocaleFromDefault; } paramsResult = utils.normalizeDynamicRouteParams(routeParams, true); if (paramsResult.hasValidParams) { params = paramsResult.params; } } // handle the actual dynamic route name being requested if (pageIsDynamic && utils.defaultRouteMatches && normalizedUrlPath === srcPathname && !paramsResult.hasValidParams && !utils.normalizeDynamicRouteParams({ ...params }, true).hasValidParams) { params = utils.defaultRouteMatches; } if (params) { matchedPath = utils.interpolateDynamicPath(srcPathname, params); req.url = utils.interpolateDynamicPath(req.url, params); } } if (pageIsDynamic || didRewrite) { var _utils_defaultRouteRegex; utils.normalizeVercelUrl(req, true, [ ...rewriteParamKeys, ...Object.keys(((_utils_defaultRouteRegex = utils.defaultRouteRegex) == null ? void 0 : _utils_defaultRouteRegex.groups) || {}) ]); } for (const key of routeParamKeys){ delete parsedUrl.query[key]; } parsedUrl.pathname = matchedPath; url.pathname = parsedUrl.pathname; const normalizeResult = await this.normalizeNextData(req, res, parsedUrl); if (normalizeResult.finished) { return; } } catch (err) { if (err instanceof DecodeError || err instanceof NormalizeError) { res.statusCode = 400; return this.renderError(null, req, res, "/_error", {}); } throw err; } } if (// Edge runtime always has minimal mode enabled. process.env.NEXT_RUNTIME !== "edge" && !this.minimalMode && defaultLocale) { const { getLocaleRedirect } = require("../shared/lib/i18n/get-locale-redirect"); const redirect = getLocaleRedirect({ defaultLocale, domainLocale, headers: req.headers, nextConfig: this.nextConfig, pathLocale: pathnameInfo.locale, urlParsed: { ...url, pathname: pathnameInfo.locale ? `/${pathnameInfo.locale}${url.pathname}` : url.pathname } }); if (redirect) { return res.redirect(redirect, TEMPORARY_REDIRECT_STATUS).body(redirect).send(); } } addRequestMeta(req, "__nextIsLocaleDomain", Boolean(domainLocale)); if (pathnameInfo.locale) { req.url = formatUrl(url); addRequestMeta(req, "__nextStrippedLocale", true); } // If we aren't in minimal mode or there is no locale in the query // string, add the locale to the query string. if (!this.minimalMode || !parsedUrl.query.__nextLocale) { // If the locale is in the pathname, add it to the query string. if (pathnameInfo.locale) { parsedUrl.query.__nextLocale = pathnameInfo.locale; } else if (defaultLocale) { parsedUrl.query.__nextLocale = defaultLocale; parsedUrl.query.__nextInferredLocaleFromDefault = "1"; } } // set incremental cache to request meta so it can // be passed down for edge functions and the fetch disk // cache can be leveraged locally if (!this.serverOptions.webServerConfig && !getRequestMeta(req, "_nextIncrementalCache")) { let protocol = "https:"; try { const parsedFullUrl = new URL(getRequestMeta(req, "__NEXT_INIT_URL") || "/", "http://n"); protocol = parsedFullUrl.protocol; } catch (_) {} const incrementalCache = this.getIncrementalCache({ requestHeaders: Object.assign({}, req.headers), requestProtocol: protocol.substring(0, protocol.length - 1) }); addRequestMeta(req, "_nextIncrementalCache", incrementalCache); globalThis.__incrementalCache = incrementalCache; } // when x-invoke-path is specified we can short short circuit resolving // we only honor this header if we are inside of a render worker to // prevent external users coercing the routing path const invokePath = req.headers["x-invoke-path"]; const useInvokePath = !useMatchedPathHeader && process.env.NEXT_RUNTIME !== "edge" && process.env.__NEXT_PRIVATE_RENDER_WORKER && invokePath; if (useInvokePath) { var _this_nextConfig_i18n1; if (req.headers["x-invoke-status"]) { const invokeQuery = req.headers["x-invoke-query"]; if (typeof invokeQuery === "string") { Object.assign(parsedUrl.query, JSON.parse(decodeURIComponent(invokeQuery))); } res.statusCode = Number(req.headers["x-invoke-status"]); let err = null; if (typeof req.headers["x-invoke-error"] === "string") { const invokeError = JSON.parse(req.headers["x-invoke-error"] || "{}"); err = new Error(invokeError.message); } 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) { parsedUrl.query.__nextLocale = invokePathnameInfo.locale; } if (parsedUrl.pathname !== parsedMatchedPath.pathname) { parsedUrl.pathname = parsedMatchedPath.pathname; addRequestMeta(req, "_nextRewroteUrl", invokePathnameInfo.pathname); addRequestMeta(req, "_nextDidRewrite", true); } const normalizeResult = normalizeLocalePath(removePathPrefix(parsedUrl.pathname, this.nextConfig.basePath || ""), ((_this_nextConfig_i18n1 = this.nextConfig.i18n) == null ? void 0 : _this_nextConfig_i18n1.locales) || []); if (normalizeResult.detectedLocale) { parsedUrl.query.__nextLocale = normalizeResult.detectedLocale; } parsedUrl.pathname = normalizeResult.pathname; for (const key of Object.keys(parsedUrl.query)){ if (!key.startsWith("__next") && !key.startsWith("_next")) { delete parsedUrl.query[key]; } } const invokeQuery = req.headers["x-invoke-query"]; if (typeof invokeQuery === "string") { Object.assign(parsedUrl.query, JSON.parse(decodeURIComponent(invokeQuery))); } if (parsedUrl.pathname.startsWith("/_next/image")) { const imageResult = await this.handleNextImageRequest(req, res, parsedUrl); if (imageResult.finished) { return; } } const nextDataResult = await this.normalizeNextData(req, res, parsedUrl); if (nextDataResult.finished) { return; } await this.handleCatchallRenderRequest(req, res, parsedUrl); return; } if (process.env.NEXT_RUNTIME !== "edge" && process.env.__NEXT_PRIVATE_RENDER_WORKER && req.headers["x-middleware-invoke"]) { const nextDataResult = await this.normalizeNextData(req, res, parsedUrl); if (nextDataResult.finished) { return; } const result = await this.handleCatchallMiddlewareRequest(req, res, parsedUrl); if (!result.finished) { res.setHeader("x-middleware-next", "1"); res.body(""); res.send(); } return; } // ensure we strip the basePath when not using an invoke header if (!(useMatchedPathHeader || useInvokePath) && pathnameInfo.basePath) { parsedUrl.pathname = removePathPrefix(parsedUrl.pathname, pathnameInfo.basePath); } res.statusCode = 200; return await this.run(req, res, parsedUrl); } catch (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) { throw err; } this.logError(getProperError(err)); res.statusCode = 500; res.body("Internal Server Error").send(); } } getRequestHandler() { return this.handleRequest.bind(this); } async handleUpgrade(_req, _socket, _head) {} setAssetPrefix(prefix) { this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, "") : ""; } /** * Runs async initialization of server. * It is idempotent, won't fire underlying initialization more than once. */ async prepare() { if (this.prepared) return; if (this.preparedPromise === null) { this.preparedPromise = this.prepareImpl().then(()=>{ this.prepared = true; this.preparedPromise = null; }); } return this.preparedPromise; } async prepareImpl() {} // Backwards compatibility async close() {} getAppPathRoutes() { const appPathRoutes = {}; Object.keys(this.appPathsManifest || {}).forEach((entry)=>{ const normalizedPath = normalizeAppPath(entry); if (!appPathRoutes[normalizedPath]) { appPathRoutes[normalizedPath] = []; } appPathRoutes[normalizedPath].push(entry); }); return appPathRoutes; } async run(req, res, parsedUrl) { return getTracer().trace(BaseServerSpan.run, async ()=>this.runImpl(req, res, parsedUrl)); } async runImpl(req, res, parsedUrl) { await this.handleCatchallRenderRequest(req, res, parsedUrl); } async pipe(fn, partialContext) { return getTracer().trace(BaseServerSpan.pipe, async ()=>this.pipeImpl(fn, partialContext)); } async pipeImpl(fn, partialContext) { const isBotRequest = isBot(partialContext.req.headers["user-agent"] || ""); const ctx = { ...partialContext, renderOpts: { ...this.renderOpts, supportsDynamicHTML: !isBotRequest, isBot: !!isBotRequest } }; const payload = await fn(ctx); if (payload === null) { return; } const { req , res } = ctx; const { body , type , revalidateOptions } = payload; if (!res.sent) { const { generateEtags , poweredByHeader , dev } = this.renderOpts; if (dev) { // In dev, we should not cache pages for any reason. res.setHeader("Cache-Control", "no-store, must-revalidate"); } return this.sendRenderResult(req, res, { result: body, type, generateEtags, poweredByHeader, options: revalidateOptions }); } } async getStaticHTML(fn, partialContext) { const ctx = { ...partialContext, renderOpts: { ...this.renderOpts, supportsDynamicHTML: false } }; const payload = await fn(ctx); if (payload === null) { return null; } return payload.body.toUnchunkedString(); } async render(req, res, pathname, query = {}, parsedUrl, internalRender = false) { return getTracer().trace(BaseServerSpan.render, async ()=>this.renderImpl(req, res, pathname, query, parsedUrl, internalRender)); } async renderImpl(req, res, pathname, query = {}, parsedUrl, internalRender = false) { var _req_url; 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.renderOpts.customServer && pathname === "/index" && !await this.hasPage("/index")) { // maintain backwards compatibility for custom server // (see custom-server integration tests) pathname = "/"; } // we allow custom servers to call render for all URLs // so check if we need to serve a static _next file or not. // we don't modify the URL for _next/data request but still // call render so we special case this to prevent an infinite loop if (!internalRender && !this.minimalMode && !query.__nextDataReq && (((_req_url = req.url) == null ? void 0 : _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 }); } async getStaticPaths({ pathname }) { var _this_getPrerenderManifest_dynamicRoutes_pathname; // `staticPaths` is intentionally set to `undefined` as it should've // been caught when checking disk data. const staticPaths = undefined; // Read whether or not fallback should exist from the manifest. const fallbackField = (_this_getPrerenderManifest_dynamicRoutes_pathname = this.getPrerenderManifest().dynamicRoutes[pathname]) == null ? void 0 : _this_getPrerenderManifest_dynamicRoutes_pathname.fallback; return { staticPaths, fallbackMode: typeof fallbackField === "string" ? "static" : fallbackField === null ? "blocking" : fallbackField }; } async renderToResponseWithComponents(requestContext, findComponentsResult) { return getTracer().trace(BaseServerSpan.renderToResponseWithComponents, async ()=>this.renderToResponseWithComponentsImpl(requestContext, findComponentsResult)); } async renderToResponseWithComponentsImpl({ req , res , pathname , renderOpts: opts }, { components , query }) { var _components_Component, _this_nextConfig_i18n, _this_nextConfig_i18n1; const is404Page = pathname === "/404"; const is500Page = pathname === "/500"; const isAppPath = components.isAppPath; const hasServerProps = !!components.getServerSideProps; let hasStaticPaths = !!components.getStaticPaths; const hasGetInitialProps = !!((_components_Component = components.Component) == null ? void 0 : _components_Component.getInitialProps); let isSSG = !!components.getStaticProps; // Compute the iSSG cache key. We use the rewroteUrl since // pages with fallback: false are allowed to be rewritten to // and we need to look up the path by the rewritten path let urlPathname = parseUrl(req.url || "").pathname || "/"; let resolvedUrlPathname = getRequestMeta(req, "_nextRewroteUrl") || urlPathname; let staticPaths; let fallbackMode; if (isAppPath) { const pathsResult = await this.getStaticPaths({ pathname, originalAppPath: components.pathname, requestHeaders: req.headers }); staticPaths = pathsResult.staticPaths; fallbackMode = pathsResult.fallbackMode; const hasFallback = typeof fallbackMode !== "undefined"; if (hasFallback) { hasStaticPaths = true; } if (hasFallback || (staticPaths == null ? void 0 : staticPaths.includes(resolvedUrlPathname)) || // this signals revalidation in deploy environments // TODO: make this more generic req.headers["x-now-route-matches"]) { isSSG = true; } else if (!this.renderOpts.dev) { const manifest = this.getPrerenderManifest(); isSSG = isSSG || !!manifest.routes[pathname === "/index" ? "/" : pathname]; } } // Toggle whether or not this is a Data request let isDataReq = !!(query.__nextDataReq || req.headers["x-nextjs-data"] && this.serverOptions.webServerConfig) && (isSSG || hasServerProps); // when we are handling a middleware prefetch and it doesn't // resolve to a static data route we bail early to avoid // unexpected SSR invocations if (!isSSG && req.headers["x-middleware-prefetch"] && !(is404Page || pathname === "/_error")) { res.setHeader("x-middleware-skip", "1"); res.body("{}").send(); return null; } delete query.__nextDataReq; // normalize req.url for SSG paths as it is not exposed // to getStaticProps and the asPath should not expose /_next/data if (isSSG && this.minimalMode && req.headers["x-matched-path"] && req.url.startsWith("/_next/data")) { req.url = this.stripNextDataPath(req.url); } if (!!req.headers["x-nextjs-data"] && (!res.statusCode || res.statusCode === 200)) { res.setHeader("x-nextjs-matched-path", `${query.__nextLocale ? `/${query.__nextLocale}` : ""}${pathname}`); } // Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later const isFlightRequest = Boolean(req.headers[RSC.toLowerCase()]); // For pages we need to ensure the correct Vary header is set too, to avoid // caching issues when navigating between pages and app if (!isAppPath && isFlightRequest) { res.setHeader("vary", RSC_VARY_HEADER); } // we need to ensure the status code if /404 is visited directly if (is404Page && !isDataReq && !isFlightRequest) { res.statusCode = 404; } // ensure correct status is set when visiting a status page // directly e.g. /500 if (STATIC_STATUS_PAGES.includes(pathname)) { res.statusCode = parseInt(pathname.slice(1), 10); } // static pages can only respond to GET/HEAD // requests so ensure we respond with 405 for // invalid requests if (!is404Page && !is500Page && pathname !== "/_error" && req.method !== "HEAD" && req.method !== "GET" && (typeof components.Component === "string" || isSSG)) { res.statusCode = 405; res.setHeader("Allow", [ "GET", "HEAD" ]); await this.renderError(null, req, res, pathname); return null; } // handle static page if (typeof components.Component === "string") { return { type: "html", // TODO: Static pages should be serialized as RenderResult body: RenderResult.fromStatic(components.Component) }; } if (!query.amp) { delete query.amp; } if (opts.supportsDynamicHTML === true) { var _components_Document; const isBotRequest = isBot(req.headers["user-agent"] || ""); const isSupportedDocument = typeof ((_components_Document = components.Document) == null ? void 0 : _components_Document.getInitialProps) !== "function" || // The built-in `Document` component also supports dynamic HTML for concurrent mode. NEXT_BUILTIN_DOCUMENT in components.Document; // Disable dynamic HTML in cases that we know it won't be generated, // so that we can continue generating a cache key when possible. // TODO-APP: should the first render for a dynamic app path // be static so we can collect revalidate and populate the // cache if there are no dynamic data requirements opts.supportsDynamicHTML = !isSSG && !isBotRequest && !query.amp && isSupportedDocument; opts.isBot = isBotRequest; } // In development, we always want to generate dynamic HTML. if (!isDataReq && isAppPath && opts.dev && opts.supportsDynamicHTML === false) { opts.supportsDynamicHTML = true; } const defaultLocale = isSSG ? (_this_nextConfig_i18n = this.nextConfig.i18n) == null ? void 0 : _this_nextConfig_i18n.defaultLocale : query.__nextDefaultLocale; const locale = query.__nextLocale; const locales = (_this_nextConfig_i18n1 = this.nextConfig.i18n) == null ? void 0 : _this_nextConfig_i18n1.locales; let previewData; let isPreviewMode = false; if (hasServerProps || isSSG) { // For the edge runtime, we don't support preview mode in SSG. if (process.env.NEXT_RUNTIME !== "edge") { const { tryGetPreviewData } = require("./api-utils/node"); previewData = tryGetPreviewData(req, res, this.renderOpts.previewProps); isPreviewMode = previewData !== false; } } if (isAppPath) { res.setHeader("vary", RSC_VARY_HEADER); // We don't clear RSC headers in development since SSG doesn't apply // These headers are cleared for SSG as we need to always generate the // full RSC response for ISR if (!this.renderOpts.dev && !isPreviewMode && isSSG && req.headers[RSC.toLowerCase()]) { if (!this.minimalMode) { isDataReq = true; } // strip header so we generate HTML still if (!isEdgeRuntime(opts.runtime) || this.serverOptions.webServerConfig) { for (const param of FLIGHT_PARAMETERS){ delete req.headers[param.toString().toLowerCase()]; } } } } let isOnDemandRevalidate = false; let revalidateOnlyGenerated = false; if (isSSG) { ({ isOnDemandRevalidate , revalidateOnlyGenerated } = checkIsOnDemandRevalidate(req, this.renderOpts.previewProps)); } if (isSSG && this.minimalMode && req.headers["x-matched-path"]) { // the url value is already correct when the matched-path header is set resolvedUrlPathname = urlPathname; } urlPathname = removeTrailingSlash(urlPathname); resolvedUrlPathname = removeTrailingSlash(resolvedUrlPathname); if (this.localeNormalizer) { resolvedUrlPathname = this.localeNormalizer.normalize(resolvedUrlPathname); } const handleRedirect = (pageData)=>{ const redirect = { destination: pageData.pageProps.__N_REDIRECT, statusCode: pageData.pageProps.__N_REDIRECT_STATUS, basePath: pageData.pageProps.__N_REDIRECT_BASE_PATH }; const statusCode = getRedirectStatus(redirect); const { basePath } = this.nextConfig; if (basePath && redirect.basePath !== false && redirect.destination.startsWith("/")) { redirect.destination = `${basePath}${redirect.destination}`; } if (redirect.destination.startsWith("/")) { redirect.destination = normalizeRepeatedSlashes(redirect.destination); } res.redirect(redirect.destination, statusCode).body(redirect.destination).send(); }; // remove /_next/data prefix from urlPathname so it matches // for direct page visit and /_next/data visit if (isDataReq) { resolvedUrlPathname = this.stripNextDataPath(resolvedUrlPathname); u