next
Version:
The React Framework
566 lines (565 loc) • 30.7 kB
JavaScript
import url from 'url';
import path from 'node:path';
import setupDebug from 'next/dist/compiled/debug';
import { getCloneableBody } from '../../body-streams';
import { filterReqHeaders, ipcForbiddenHeaders } from '../server-ipc/utils';
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 { getRelativeURL } 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 { NextDataPathnameNormalizer } from '../../normalizers/request/next-data';
import { BasePathPathnameNormalizer } from '../../normalizers/request/base-path';
import { addRequestMeta } from '../../request-meta';
import { compileNonPath, matchHas, parseDestination, prepareDestination } from '../../../shared/lib/router/utils/prepare-destination';
import { NEXT_REWRITTEN_PATH_HEADER, NEXT_REWRITTEN_QUERY_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, RSC_HEADER } from '../../../client/components/app-router-headers';
import { getSelectedParams } from '../../../client/components/router-reducer/compute-changed-path';
import { isInterceptionRouteRewrite } from '../../../lib/generate-interception-routes-rewrites';
import { parseAndValidateFlightRouterState } from '../../app-render/parse-and-validate-flight-router-state';
const debug = setupDebug('next:router-server:resolve-routes');
export function getResolveRoutes(fsChecker, config, opts, renderServer, renderServerOpts, 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 _req_socket, _req_headers_xforwardedproto;
let finished = false;
let resHeaders = {};
let matchedOutput = null;
let parsedUrl = url.parse(req.url || '', true);
let didRewrite = false;
const urlParts = (req.url || '').split('?', 1);
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 = (req == null ? void 0 : (_req_socket = req.socket) == null ? void 0 : _req_socket.encrypted) || ((_req_headers_xforwardedproto = req.headers['x-forwarded-proto']) == null ? void 0 : _req_headers_xforwardedproto.includes('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, 'initURL', initUrl);
addRequestMeta(req, 'initQuery', {
...parsedUrl.query
});
addRequestMeta(req, 'initProtocol', protocol);
if (!isUpgradeReq) {
addRequestMeta(req, 'clonableBody', 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;
addRequestMeta(req, 'defaultLocale', defaultLocale);
addRequestMeta(req, 'locale', 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'))) {
addRequestMeta(req, 'isNextDataReq', true);
}
if (config.useFileSystemPublicRoutes || didRewrite) {
return pageOutput;
}
}
}
}
const normalizers = {
basePath: config.basePath && config.basePath !== '/' ? new BasePathPathnameNormalizer(config.basePath) : undefined,
data: new NextDataPathnameNormalizer(fsChecker.buildId)
};
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.exportPathMapRoutes && route.name === 'before_files_end') {
for (const exportPathMapRoute of fsChecker.exportPathMapRoutes){
const result = await handleRoute(exportPathMapRoute);
if (result) {
return result;
}
}
}
if (route.name === 'middleware_next_data' && parsedUrl.pathname) {
var _fsChecker_getMiddlewareMatchers;
if ((_fsChecker_getMiddlewareMatchers = fsChecker.getMiddlewareMatchers()) == null ? void 0 : _fsChecker_getMiddlewareMatchers.length) {
var _normalizers_basePath;
let normalized = parsedUrl.pathname;
// Remove the base path if it exists.
const hadBasePath = (_normalizers_basePath = normalizers.basePath) == null ? void 0 : _normalizers_basePath.match(parsedUrl.pathname);
if (hadBasePath && normalizers.basePath) {
normalized = normalizers.basePath.normalize(normalized, true);
}
let updated = false;
if (normalizers.data.match(normalized)) {
updated = true;
addRequestMeta(req, 'isNextDataReq', true);
normalized = normalizers.data.normalize(normalized, true);
}
if (config.i18n) {
const curLocaleResult = normalizeLocalePath(normalized, config.i18n.locales);
if (curLocaleResult.detectedLocale) {
addRequestMeta(req, 'locale', curLocaleResult.detectedLocale);
}
}
// If we updated the pathname, and it had a base path, re-add the
// base path.
if (updated) {
if (hadBasePath) {
normalized = path.posix.join(config.basePath, normalized);
}
// Re-add the trailing slash (if required).
normalized = maybeAddTrailingSlash(normalized);
parsedUrl.pathname = normalized;
}
}
}
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) {
addRequestMeta(req, 'locale', 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)) {
if (ensureMiddleware) {
await ensureMiddleware(req.url);
}
const serverResult = await (renderServer == null ? void 0 : renderServer.initialize(renderServerOpts));
if (!serverResult) {
throw Object.defineProperty(new Error(`Failed to initialize render server "middleware"`), "__NEXT_ERROR_CODE", {
value: "E222",
enumerable: false,
configurable: true
});
}
addRequestMeta(req, 'invokePath', '');
addRequestMeta(req, 'invokeOutput', '');
addRequestMeta(req, 'invokeQuery', {});
addRequestMeta(req, 'middlewareInvoke', true);
debug('invoking middleware', req.url, req.headers);
let middlewareRes = undefined;
let bodyStream = undefined;
try {
try {
await serverResult.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'
].includes(key)) {
continue;
}
// for set-cookie, the header shouldn't be added to the response
// as it's only needed for the request to the middleware function.
if (key === 'x-middleware-set-cookie') {
req.headers[key] = value;
continue;
}
if (value) {
resHeaders[key] = value;
req.headers[key] = value;
}
}
if (middlewareHeaders['x-middleware-rewrite']) {
const value = middlewareHeaders['x-middleware-rewrite'];
const destination = getRelativeURL(value, initUrl);
resHeaders['x-middleware-rewrite'] = destination;
parsedUrl = url.parse(destination, true);
if (parsedUrl.protocol) {
return {
parsedUrl,
resHeaders,
finished: true
};
}
if (config.i18n) {
const curLocaleResult = normalizeLocalePath(parsedUrl.pathname || '', config.i18n.locales);
if (curLocaleResult.detectedLocale) {
addRequestMeta(req, 'locale', curLocaleResult.detectedLocale);
}
}
}
if (middlewareHeaders['location']) {
const value = middlewareHeaders['location'];
const rel = getRelativeURL(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) {
let rewriteParams = params;
try {
// An interception rewrite might reference a dynamic param for a route the user
// is currently on, which wouldn't be extractable from the matched route params.
// This attempts to extract the dynamic params from the provided router state.
if (isInterceptionRouteRewrite(route)) {
const stateHeader = req.headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()];
if (stateHeader) {
rewriteParams = {
...getSelectedParams(parseAndValidateFlightRouterState(stateHeader)),
...params
};
}
}
} catch (err) {
// this is a no-op -- we couldn't extract dynamic params from the provided router state,
// so we'll just use the params from the route matcher
}
// We extract the search params of the destination so we can set it on
// the response headers. We don't want to use the following
// `parsedDestination` as the query object is mutated.
const { search: destinationSearch, pathname: destinationPathname } = parseDestination({
destination: route.destination,
params: rewriteParams,
query: parsedUrl.query
});
const { parsedDestination } = prepareDestination({
appendParamsToQuery: true,
destination: route.destination,
params: rewriteParams,
query: parsedUrl.query
});
if (parsedDestination.protocol) {
return {
// @ts-expect-error custom ParsedUrl
parsedUrl: parsedDestination,
finished: true
};
}
// Set the rewrite headers only if this is a RSC request.
if (req.headers[RSC_HEADER.toLowerCase()] === '1') {
// We set the rewritten path and query headers on the response now
// that we know that the it's not an external rewrite.
if (parsedUrl.pathname !== destinationPathname) {
res.setHeader(NEXT_REWRITTEN_PATH_HEADER, destinationPathname);
}
if (destinationSearch) {
res.setHeader(NEXT_REWRITTEN_QUERY_HEADER, // remove the leading ? from the search
destinationSearch.slice(1));
}
}
if (config.i18n) {
const curLocaleResult = normalizeLocalePath(removePathPrefix(parsedDestination.pathname, config.basePath), config.i18n.locales);
if (curLocaleResult.detectedLocale) {
addRequestMeta(req, 'locale', 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