import url from "url"; import setupDebug from "next/dist/compiled/debug"; import { getCloneableBody } from "../../body-streams"; import { filterReqHeaders, ipcForbiddenHeaders } from "../server-ipc/utils"; import { stringifyQuery } from "../../server-route-utils"; import { formatHostname } from "../format-hostname"; import { toNodeOutgoingHttpHeaders } from "../../web/utils"; import { isAbortError } from "../../pipe-readable"; import { getHostname } from "../../../shared/lib/get-hostname"; import { getRedirectStatus } from "../../../lib/redirect-status"; import { normalizeRepeatedSlashes } from "../../../shared/lib/utils"; import { relativizeURL } from "../../../shared/lib/router/utils/relativize-url"; import { addPathPrefix } from "../../../shared/lib/router/utils/add-path-prefix"; import { pathHasPrefix } from "../../../shared/lib/router/utils/path-has-prefix"; import { detectDomainLocale } from "../../../shared/lib/i18n/detect-domain-locale"; import { normalizeLocalePath } from "../../../shared/lib/i18n/normalize-locale-path"; import { removePathPrefix } from "../../../shared/lib/router/utils/remove-path-prefix"; import { addRequestMeta } from "../../request-meta"; import { compileNonPath, matchHas, prepareDestination } from "../../../shared/lib/router/utils/prepare-destination"; import { createRequestResponseMocks } from "../mock-request"; import "../../node-polyfill-web-streams"; const debug = setupDebug("next:router-server:resolve-routes"); export function getResolveRoutes(fsChecker, config, opts, renderWorkers, renderWorkerOpts, ensureMiddleware) { const routes = [ // _next/data with middleware handling { match: ()=>({}), name: "middleware_next_data" }, ...opts.minimalMode ? [] : fsChecker.headers, ...opts.minimalMode ? [] : fsChecker.redirects, // check middleware (using matchers) { match: ()=>({}), name: "middleware" }, ...opts.minimalMode ? [] : fsChecker.rewrites.beforeFiles, // check middleware (using matchers) { match: ()=>({}), name: "before_files_end" }, // we check exact matches on fs before continuing to // after files rewrites { match: ()=>({}), name: "check_fs" }, ...opts.minimalMode ? [] : fsChecker.rewrites.afterFiles, // we always do the check: true handling before continuing to // fallback rewrites { check: true, match: ()=>({}), name: "after files check: true" }, ...opts.minimalMode ? [] : fsChecker.rewrites.fallback ]; async function resolveRoutes({ req, res, isUpgradeReq, invokedOutputs }) { var _this; let finished = false; let resHeaders = {}; let matchedOutput = null; let parsedUrl = url.parse(req.url || "", true); let didRewrite = false; 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(/(\\|\/\/)/)) { parsedUrl = url.parse(normalizeRepeatedSlashes(req.url), true); return { parsedUrl, resHeaders, finished: true, statusCode: 308 }; } // TODO: inherit this from higher up const protocol = ((_this = req == null ? void 0 : req.socket) == null ? void 0 : _this.encrypted) || req.headers["x-forwarded-proto"] === "https" ? "https" : "http"; // When there are hostname and port we build an absolute URL const initUrl = config.experimental.trustHostHeader ? `https://${req.headers.host || "localhost"}${req.url}` : opts.port ? `${protocol}://${formatHostname(opts.hostname || "localhost")}:${opts.port}${req.url}` : req.url || ""; addRequestMeta(req, "__NEXT_INIT_URL", initUrl); addRequestMeta(req, "__NEXT_INIT_QUERY", { ...parsedUrl.query }); addRequestMeta(req, "_protocol", protocol); if (!isUpgradeReq) { addRequestMeta(req, "__NEXT_CLONABLE_BODY", getCloneableBody(req)); } const maybeAddTrailingSlash = (pathname)=>{ if (config.trailingSlash && !config.skipMiddlewareUrlNormalize && !pathname.endsWith("/")) { return `${pathname}/`; } return pathname; }; let domainLocale; let defaultLocale; let initialLocaleResult = undefined; if (config.i18n) { var _parsedUrl_pathname; const hadTrailingSlash = (_parsedUrl_pathname = parsedUrl.pathname) == null ? void 0 : _parsedUrl_pathname.endsWith("/"); const hadBasePath = pathHasPrefix(parsedUrl.pathname || "", config.basePath); initialLocaleResult = normalizeLocalePath(removePathPrefix(parsedUrl.pathname || "/", config.basePath), config.i18n.locales); domainLocale = detectDomainLocale(config.i18n.domains, getHostname(parsedUrl, req.headers)); defaultLocale = (domainLocale == null ? void 0 : domainLocale.defaultLocale) || config.i18n.defaultLocale; parsedUrl.query.__nextDefaultLocale = defaultLocale; parsedUrl.query.__nextLocale = initialLocaleResult.detectedLocale || defaultLocale; // ensure locale is present for resolving routes if (!initialLocaleResult.detectedLocale && !initialLocaleResult.pathname.startsWith("/_next/")) { parsedUrl.pathname = addPathPrefix(initialLocaleResult.pathname === "/" ? `/${defaultLocale}` : addPathPrefix(initialLocaleResult.pathname || "", `/${defaultLocale}`), hadBasePath ? config.basePath : ""); if (hadTrailingSlash) { parsedUrl.pathname = maybeAddTrailingSlash(parsedUrl.pathname); } } } const checkLocaleApi = (pathname)=>{ if (config.i18n && pathname === urlNoQuery && (initialLocaleResult == null ? void 0 : initialLocaleResult.detectedLocale) && pathHasPrefix(initialLocaleResult.pathname, "/api")) { return true; } }; async function checkTrue() { const pathname = parsedUrl.pathname || ""; if (checkLocaleApi(pathname)) { return; } if (!(invokedOutputs == null ? void 0 : invokedOutputs.has(pathname))) { const output = await fsChecker.getItem(pathname); if (output) { if (config.useFileSystemPublicRoutes || didRewrite || output.type !== "appFile" && output.type !== "pageFile") { return output; } } } const dynamicRoutes = fsChecker.getDynamicRoutes(); let curPathname = parsedUrl.pathname; if (config.basePath) { if (!pathHasPrefix(curPathname || "", config.basePath)) { return; } curPathname = (curPathname == null ? void 0 : curPathname.substring(config.basePath.length)) || "/"; } const localeResult = fsChecker.handleLocale(curPathname || ""); for (const route of dynamicRoutes){ // when resolving fallback: false the // render worker may return a no-fallback response // which signals we need to continue resolving. // TODO: optimize this to collect static paths // to use at the routing layer if (invokedOutputs == null ? void 0 : invokedOutputs.has(route.page)) { continue; } const params = route.match(localeResult.pathname); if (params) { const pageOutput = await fsChecker.getItem(addPathPrefix(route.page, config.basePath || "")); // i18n locales aren't matched for app dir if ((pageOutput == null ? void 0 : pageOutput.type) === "appFile" && (initialLocaleResult == null ? void 0 : initialLocaleResult.detectedLocale)) { continue; } if (pageOutput && (curPathname == null ? void 0 : curPathname.startsWith("/_next/data"))) { parsedUrl.query.__nextDataReq = "1"; } if (config.useFileSystemPublicRoutes || didRewrite) { return pageOutput; } } } } async function handleRoute(route) { let curPathname = parsedUrl.pathname || "/"; if (config.i18n && route.internal) { const hadTrailingSlash = curPathname.endsWith("/"); if (config.basePath) { curPathname = removePathPrefix(curPathname, config.basePath); } const hadBasePath = curPathname !== parsedUrl.pathname; const localeResult = normalizeLocalePath(curPathname, config.i18n.locales); const isDefaultLocale = localeResult.detectedLocale === defaultLocale; if (isDefaultLocale) { curPathname = localeResult.pathname === "/" && hadBasePath ? config.basePath : addPathPrefix(localeResult.pathname, hadBasePath ? config.basePath : ""); } else if (hadBasePath) { curPathname = curPathname === "/" ? config.basePath : addPathPrefix(curPathname, config.basePath); } if ((isDefaultLocale || hadBasePath) && hadTrailingSlash) { curPathname = maybeAddTrailingSlash(curPathname); } } let params = route.match(curPathname); if ((route.has || route.missing) && params) { const hasParams = matchHas(req, parsedUrl.query, route.has, route.missing); if (hasParams) { Object.assign(params, hasParams); } else { params = false; } } if (params) { if (fsChecker.interceptionRoutes && route.name === "before_files_end") { for (const interceptionRoute of fsChecker.interceptionRoutes){ const result = await handleRoute(interceptionRoute); if (result) { return result; } } } if (route.name === "middleware_next_data") { var _fsChecker_getMiddlewareMatchers; if ((_fsChecker_getMiddlewareMatchers = fsChecker.getMiddlewareMatchers()) == null ? void 0 : _fsChecker_getMiddlewareMatchers.length) { var _parsedUrl_pathname; const nextDataPrefix = addPathPrefix(`/_next/data/${fsChecker.buildId}/`, config.basePath); if (((_parsedUrl_pathname = parsedUrl.pathname) == null ? void 0 : _parsedUrl_pathname.startsWith(nextDataPrefix)) && parsedUrl.pathname.endsWith(".json")) { parsedUrl.query.__nextDataReq = "1"; parsedUrl.pathname = parsedUrl.pathname.substring(nextDataPrefix.length - 1); parsedUrl.pathname = parsedUrl.pathname.substring(0, parsedUrl.pathname.length - ".json".length); parsedUrl.pathname = addPathPrefix(parsedUrl.pathname || "", config.basePath); parsedUrl.pathname = parsedUrl.pathname === "/index" ? "/" : parsedUrl.pathname; parsedUrl.pathname = maybeAddTrailingSlash(parsedUrl.pathname); } } } if (route.name === "check_fs") { const pathname = parsedUrl.pathname || ""; if ((invokedOutputs == null ? void 0 : invokedOutputs.has(pathname)) || checkLocaleApi(pathname)) { return; } const output = await fsChecker.getItem(pathname); if (output && !(config.i18n && (initialLocaleResult == null ? void 0 : initialLocaleResult.detectedLocale) && pathHasPrefix(pathname, "/api"))) { if (config.useFileSystemPublicRoutes || didRewrite || output.type !== "appFile" && output.type !== "pageFile") { matchedOutput = output; if (output.locale) { parsedUrl.query.__nextLocale = output.locale; } return { parsedUrl, resHeaders, finished: true, matchedOutput }; } } } if (!opts.minimalMode && route.name === "middleware") { const match = fsChecker.getMiddlewareMatchers(); if (// @ts-expect-error BaseNextRequest stuff (match == null ? void 0 : match(parsedUrl.pathname, req, parsedUrl.query)) && (!ensureMiddleware || await (ensureMiddleware == null ? void 0 : ensureMiddleware().then(()=>true).catch(()=>false)))) { var _this; const workerResult = await ((_this = renderWorkers.app || renderWorkers.pages) == null ? void 0 : _this.initialize(renderWorkerOpts)); if (!workerResult) { throw new Error(`Failed to initialize render worker "middleware"`); } const invokeHeaders = { "x-invoke-path": "", "x-invoke-query": "", "x-invoke-output": "", "x-middleware-invoke": "1" }; Object.assign(req.headers, invokeHeaders); debug("invoking middleware", req.url, invokeHeaders); let middlewareRes = undefined; let bodyStream = undefined; try { var _renderWorkers_pages; let readableController; const { res: mockedRes } = await createRequestResponseMocks({ url: req.url || "/", method: req.method || "GET", headers: filterReqHeaders(invokeHeaders, ipcForbiddenHeaders), resWriter (chunk) { readableController.enqueue(Buffer.from(chunk)); return true; } }); const initResult = await ((_renderWorkers_pages = renderWorkers.pages) == null ? void 0 : _renderWorkers_pages.initialize(renderWorkerOpts)); mockedRes.on("close", ()=>{ readableController.close(); }); try { await (initResult == null ? void 0 : initResult.requestHandler(req, res, parsedUrl)); } catch (err) { if (!("result" in err) || !("response" in err.result)) { throw err; } middlewareRes = err.result.response; res.statusCode = middlewareRes.status; if (middlewareRes.body) { bodyStream = middlewareRes.body; } else if (middlewareRes.status) { bodyStream = new ReadableStream({ start (controller) { controller.enqueue(""); controller.close(); } }); } } } catch (e) { // If the client aborts before we can receive a response object // (when the headers are flushed), then we can early exit without // further processing. if (isAbortError(e)) { return { parsedUrl, resHeaders, finished: true }; } throw e; } if (res.closed || res.finished || !middlewareRes) { return { parsedUrl, resHeaders, finished: true }; } const middlewareHeaders = toNodeOutgoingHttpHeaders(middlewareRes.headers); debug("middleware res", middlewareRes.status, middlewareHeaders); if (middlewareHeaders["x-middleware-override-headers"]) { const overriddenHeaders = new Set(); let overrideHeaders = middlewareHeaders["x-middleware-override-headers"]; if (typeof overrideHeaders === "string") { overrideHeaders = overrideHeaders.split(","); } for (const key of overrideHeaders){ overriddenHeaders.add(key.trim()); } delete middlewareHeaders["x-middleware-override-headers"]; // Delete headers. for (const key of Object.keys(req.headers)){ if (!overriddenHeaders.has(key)) { delete req.headers[key]; } } // Update or add headers. for (const key of overriddenHeaders.keys()){ const valueKey = "x-middleware-request-" + key; const newValue = middlewareHeaders[valueKey]; const oldValue = req.headers[key]; if (oldValue !== newValue) { req.headers[key] = newValue === null ? undefined : newValue; } delete middlewareHeaders[valueKey]; } } if (!middlewareHeaders["x-middleware-rewrite"] && !middlewareHeaders["x-middleware-next"] && !middlewareHeaders["location"]) { middlewareHeaders["x-middleware-refresh"] = "1"; } delete middlewareHeaders["x-middleware-next"]; for (const [key, value] of Object.entries({ ...filterReqHeaders(middlewareHeaders, ipcForbiddenHeaders) })){ if ([ "content-length", "x-middleware-rewrite", "x-middleware-redirect", "x-middleware-refresh", "x-middleware-invoke", "x-invoke-path", "x-invoke-query" ].includes(key)) { continue; } if (value) { resHeaders[key] = value; req.headers[key] = value; } } if (middlewareHeaders["x-middleware-rewrite"]) { const value = middlewareHeaders["x-middleware-rewrite"]; const rel = relativizeURL(value, initUrl); resHeaders["x-middleware-rewrite"] = rel; const query = parsedUrl.query; parsedUrl = url.parse(rel, true); if (parsedUrl.protocol) { return { parsedUrl, resHeaders, finished: true }; } // keep internal query state for (const key of Object.keys(query)){ if (key.startsWith("_next") || key.startsWith("__next")) { parsedUrl.query[key] = query[key]; } } if (config.i18n) { const curLocaleResult = normalizeLocalePath(parsedUrl.pathname || "", config.i18n.locales); if (curLocaleResult.detectedLocale) { parsedUrl.query.__nextLocale = curLocaleResult.detectedLocale; } } } if (middlewareHeaders["location"]) { const value = middlewareHeaders["location"]; const rel = relativizeURL(value, initUrl); resHeaders["location"] = rel; parsedUrl = url.parse(rel, true); return { parsedUrl, resHeaders, finished: true, statusCode: middlewareRes.status }; } if (middlewareHeaders["x-middleware-refresh"]) { return { parsedUrl, resHeaders, finished: true, bodyStream, statusCode: middlewareRes.status }; } } } // handle redirect if (("statusCode" in route || "permanent" in route) && route.destination) { const { parsedDestination } = prepareDestination({ appendParamsToQuery: false, destination: route.destination, params: params, query: parsedUrl.query }); const { query } = parsedDestination; delete parsedDestination.query; parsedDestination.search = stringifyQuery(req, query); parsedDestination.pathname = normalizeRepeatedSlashes(parsedDestination.pathname); return { finished: true, // @ts-expect-error custom ParsedUrl parsedUrl: parsedDestination, statusCode: getRedirectStatus(route) }; } // handle headers if (route.headers) { const hasParams = Object.keys(params).length > 0; for (const header of route.headers){ let { key, value } = header; if (hasParams) { key = compileNonPath(key, params); value = compileNonPath(value, params); } if (key.toLowerCase() === "set-cookie") { if (!Array.isArray(resHeaders[key])) { const val = resHeaders[key]; resHeaders[key] = typeof val === "string" ? [ val ] : []; } resHeaders[key].push(value); } else { resHeaders[key] = value; } } } // handle rewrite if (route.destination) { const { parsedDestination } = prepareDestination({ appendParamsToQuery: true, destination: route.destination, params: params, query: parsedUrl.query }); if (parsedDestination.protocol) { return { // @ts-expect-error custom ParsedUrl parsedUrl: parsedDestination, finished: true }; } if (config.i18n) { const curLocaleResult = normalizeLocalePath(removePathPrefix(parsedDestination.pathname, config.basePath), config.i18n.locales); if (curLocaleResult.detectedLocale) { parsedUrl.query.__nextLocale = curLocaleResult.detectedLocale; } } didRewrite = true; parsedUrl.pathname = parsedDestination.pathname; Object.assign(parsedUrl.query, parsedDestination.query); } // handle check: true if (route.check) { const output = await checkTrue(); if (output) { return { parsedUrl, resHeaders, finished: true, matchedOutput: output }; } } } } for (const route of routes){ const result = await handleRoute(route); if (result) { return result; } } return { finished, parsedUrl, resHeaders, matchedOutput }; } return resolveRoutes; } //# sourceMappingURL=resolve-routes.js.map