Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1162,11 +1162,12 @@ export default createAppRscHandler({
matchRoute,
${
middlewarePath
? `runMiddleware({ cleanPathname, context, isDataRequest, request }) {
? `runMiddleware({ cleanPathname, context, hadBasePath, isDataRequest, request }) {
return __applyAppMiddleware({
basePath: __basePath,
cleanPathname,
context,
hadBasePath,
filePath: ${JSON.stringify(middlewarePath ? normalizePathSeparators(middlewarePath) : "")},
i18nConfig: __i18nConfig,
isDataRequest,
Expand Down
14 changes: 7 additions & 7 deletions packages/vinext/src/server/app-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ApplyAppMiddlewareOptions = {
basePath?: string;
cleanPathname: string;
context: AppMiddlewareContext;
hadBasePath?: boolean;
i18nConfig?: NextI18nConfig | null;
/**
* Whether the inbound request was recognized as a `_next/data` fetch from
Expand All @@ -42,6 +43,7 @@ export type ApplyAppMiddlewareResult =
| {
kind: "continue";
cleanPathname: string;
rewritten: boolean;
search: string | null;
}
| {
Expand Down Expand Up @@ -210,6 +212,7 @@ export async function applyAppMiddleware(
const forwarded = applyForwardedMiddlewareContext(options.request, options.context);
const middlewareRequest = requestWithoutFlightHeaders(options.request);
let cleanPathname = options.cleanPathname;
let rewritten = false;
let search: string | null = null;

if (forwarded.rewriteUrl) {
Expand All @@ -226,6 +229,7 @@ export async function applyAppMiddleware(
}
const rewriteParsed = new URL(forwarded.rewriteUrl, middlewareRequest.url);
cleanPathname = rewriteParsed.pathname;
rewritten = true;
search = rewriteParsed.search;
} catch (e) {
console.error("[vinext] Failed to apply forwarded middleware rewrite:", e);
Expand All @@ -236,12 +240,7 @@ export async function applyAppMiddleware(
if (!forwarded.applied) {
const result = await executeMiddleware({
basePath: options.basePath,
// The App Router only reaches middleware when the request was under
// basePath (already stripped by normalizeRscRequest) or basePath is
// empty — see the basePathState comment in app-rsc-handler.ts. The
// request URL here is basePath-stripped, so hadBasePath cannot be
// derived from it and must be asserted explicitly.
hadBasePath: true,
hadBasePath: options.hadBasePath ?? true,
filePath: options.filePath,
i18nConfig: options.i18nConfig,
isDataRequest: options.isDataRequest,
Expand Down Expand Up @@ -286,6 +285,7 @@ export async function applyAppMiddleware(
}
const rewriteParsed = new URL(result.rewriteUrl, middlewareRequest.url);
cleanPathname = rewriteParsed.pathname;
rewritten = true;
search = rewriteParsed.search;
}
}
Expand All @@ -296,5 +296,5 @@ export async function applyAppMiddleware(
processMiddlewareHeaders(options.context.headers);
}

return { kind: "continue", cleanPathname, search };
return { kind: "continue", cleanPathname, rewritten, search };
}
94 changes: 63 additions & 31 deletions packages/vinext/src/server/app-rsc-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type AppRscMiddlewareContext = AppMiddlewareContext;
type RunAppMiddlewareOptions = {
cleanPathname: string;
context: AppRscMiddlewareContext;
hadBasePath: boolean;
isDataRequest: boolean;
request: Request;
};
Expand Down Expand Up @@ -346,12 +347,17 @@ function createMissingServerActionResponse(
return createServerActionNotFoundResponse();
}

// TODO(#1333): once App Router supports `basePath: false` rules (see
// `normalizeRscRequest` — it 404s out-of-basePath requests before they
// reach this code), pass `hadBasePath` here and skip the prefix when
// false, mirroring the same guard in `prod-server.ts` and `deploy.ts`.
function redirectDestinationWithBasePath(destination: string, basePath: string): string {
if (!basePath || isExternalUrl(destination) || hasBasePath(destination, basePath)) {
function redirectDestinationWithBasePath(
destination: string,
basePath: string,
hadBasePath: boolean,
): string {
if (
!basePath ||
!hadBasePath ||
isExternalUrl(destination) ||
hasBasePath(destination, basePath)
) {
return destination;
}
return basePath + destination;
Expand Down Expand Up @@ -482,7 +488,16 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
if (originBlock) return originBlock;
}

const normalized = normalizeRscRequest(request, options.basePath);
const canHandleOutsideBasePath =
Boolean(options.runMiddleware) ||
[
...options.configRedirects,
...options.configRewrites.beforeFiles,
...options.configRewrites.afterFiles,
...options.configRewrites.fallback,
...options.configHeaders,
].some((rule) => rule.basePath === false);
const normalized = normalizeRscRequest(request, options.basePath, canHandleOutsideBasePath);
if (normalized instanceof Response) return normalized;

const {
Expand All @@ -492,6 +507,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
mountedSlotsHeader,
renderMode,
clientReuseManifest,
hadBasePath,
} = normalized;
let { pathname, cleanPathname } = normalized;
let resolvedUrl = cleanPathname + url.search;
Expand All @@ -505,13 +521,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
// "should have the canonical url pathname on rewrite"
const canonicalPathname = cleanPathname;

// The request reached this point so it was either under basePath (stripped
// by normalizeRscRequest) or basePath is empty. In both cases the matcher
// gating below treats default (basePath: true) rules as eligible. The App
// Router does not yet support `basePath: false` rules — they would need a
// pre-strip hook in normalizeRscRequest to fire. Tracked as follow-up to
// issue #1333.
const basePathState = { basePath: options.basePath, hadBasePath: true };
const basePathState = { basePath: options.basePath, hadBasePath };

if (
pathname === VINEXT_PRERENDER_STATIC_PARAMS_PATH ||
Expand All @@ -532,7 +542,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(

const trailingSlashRedirect = normalizeTrailingSlash(
pathname,
options.basePath,
hadBasePath ? options.basePath : "",
options.trailingSlash,
url.search,
);
Expand All @@ -559,7 +569,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
);
if (redirect) {
const destination = sanitizeDestination(
redirectDestinationWithBasePath(redirect.destination, options.basePath),
redirectDestinationWithBasePath(redirect.destination, options.basePath, hadBasePath),
);
// For RSC navigations `createRscRedirectLocation` recomputes the
// cache-busting `_rsc` param onto the Location. For plain (document)
Expand Down Expand Up @@ -592,11 +602,13 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
status: null,
};
let didMiddlewareRewrite = false;
let didMiddlewareRewritePathname = false;

if (options.runMiddleware) {
const middlewareResult = await options.runMiddleware({
cleanPathname,
context: middlewareContext,
hadBasePath,
isDataRequest: isMiddlewareDataRequest,
request: userlandRequest,
});
Expand All @@ -610,7 +622,8 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
}

cleanPathname = middlewareResult.cleanPathname;
didMiddlewareRewrite = cleanPathname !== normalized.cleanPathname;
didMiddlewareRewrite = middlewareResult.rewritten;
didMiddlewareRewritePathname = cleanPathname !== normalized.cleanPathname;
if (middlewareResult.search !== null) {
url.search = middlewareResult.search;
}
Expand All @@ -619,6 +632,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(

const scriptNonce = getScriptNonceFromHeaderSources(request.headers, middlewareContext.headers);
const postMiddlewareRequestContext = buildPostMwRequestContext(userlandRequest);
let filesystemRouteEligible = hadBasePath || didMiddlewareRewrite;

// Rewrites (beforeFiles, afterFiles, fallback) use `matchPathname` from
// above to splice in the default locale before matching. Route matching
Expand Down Expand Up @@ -646,10 +660,11 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
if (beforeFilesRewrite) {
resolvedUrl = mergeRewriteQuery(resolvedUrl, beforeFilesRewrite);
cleanPathname = pathnameForResolvedUrl(resolvedUrl);
filesystemRouteEligible = true;
}
}

if (isImageOptimizationPath(cleanPathname)) {
if (filesystemRouteEligible && isImageOptimizationPath(cleanPathname)) {
const imageRedirect = resolveDevImageRedirect(
url,
[
Expand All @@ -664,7 +679,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
return Response.redirect(new URL(imageRedirect, url.origin).href, 302);
}

if (options.handleMetadataRouteRequest) {
if (filesystemRouteEligible && options.handleMetadataRouteRequest) {
const metadataRouteResponse = await options.handleMetadataRouteRequest(cleanPathname);
if (metadataRouteResponse) {
applyConfigHeadersToResponse(metadataRouteResponse.headers, {
Expand All @@ -678,13 +693,15 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
}
}

const publicFileResponse = resolvePublicFileRoute({
cleanPathname,
middlewareContext,
pathname,
publicFiles: options.publicFiles,
request,
});
const publicFileResponse = filesystemRouteEligible
? resolvePublicFileRoute({
cleanPathname,
middlewareContext,
pathname,
publicFiles: options.publicFiles,
request,
})
: null;
if (publicFileResponse) {
options.clearRequestContext();
return publicFileResponse;
Expand Down Expand Up @@ -715,7 +732,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
// page renders, so there is no stale-value risk for ordinary page renders.
// For action requests we intentionally do not re-run rewrites — actions
// are always processed against the cleanPathname they were posted to.
const preActionMatch = options.matchRoute(cleanPathname);
const preActionMatch = filesystemRouteEligible ? options.matchRoute(cleanPathname) : null;
if (preActionMatch) {
setRootParams(pickRootParams(preActionMatch.params, preActionMatch.route.rootParamNames));
}
Expand All @@ -726,7 +743,12 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(

const isPostRequest = request.method.toUpperCase() === "POST";
let progressiveActionResult: Response | ProgressiveActionFormStateResult | null = null;
if (isPostRequest && contentType.startsWith("multipart/form-data") && !actionId) {
if (
filesystemRouteEligible &&
isPostRequest &&
contentType.startsWith("multipart/form-data") &&
!actionId
) {
if (options.handleProgressiveActionRequest) {
progressiveActionResult = await options.handleProgressiveActionRequest({
actionId,
Expand Down Expand Up @@ -766,7 +788,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
}

const serverActionResponse =
isPostRequest && actionId && options.handleServerActionRequest
filesystemRouteEligible && isPostRequest && actionId && options.handleServerActionRequest
? await options.handleServerActionRequest({
actionId,
cleanPathname,
Expand All @@ -780,19 +802,20 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
})
: null;
if (serverActionResponse) return serverActionResponse;
if (isPostRequest && actionId && !options.handleServerActionRequest) {
if (filesystemRouteEligible && isPostRequest && actionId && !options.handleServerActionRequest) {
return createMissingServerActionResponse(options, actionId);
}

let match = preActionMatch;
const renderPagesForMatchKind = async (
matchKind: "dynamic" | "static",
): Promise<Response | null> => {
if (!filesystemRouteEligible) return null;
const response =
match === null || match.route.isDynamic
? ((await options.renderPagesFallback?.({
appRouteMatch: match ?? null,
allowRscDocumentFallback: didMiddlewareRewrite,
allowRscDocumentFallback: didMiddlewareRewritePathname,
isDataRequest,
isRscRequest,
matchKind,
Expand Down Expand Up @@ -839,6 +862,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
if (!afterFilesRewrite) continue;
resolvedUrl = mergeRewriteQuery(resolvedUrl, afterFilesRewrite);
cleanPathname = pathnameForResolvedUrl(resolvedUrl);
filesystemRouteEligible = true;
match = options.matchRoute(cleanPathname);
const rewrittenStaticPagesResponse = await renderPagesForMatchKind("static");
if (rewrittenStaticPagesResponse) {
Expand Down Expand Up @@ -881,6 +905,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
if (!fallbackRewrite) continue;
resolvedUrl = mergeRewriteQuery(resolvedUrl, fallbackRewrite);
cleanPathname = pathnameForResolvedUrl(resolvedUrl);
filesystemRouteEligible = true;
match = options.matchRoute(cleanPathname);
const rewrittenStaticPagesResponse = await renderPagesForMatchKind("static");
if (rewrittenStaticPagesResponse) {
Expand All @@ -896,6 +921,13 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
}
}

if (!filesystemRouteEligible) {
options.clearRequestContext();
const headers = new Headers();
mergeMiddlewareResponseHeaders(headers, middlewareContext.headers);
return notFoundResponse({ headers });
}

if (pagesDataRequest) {
options.clearRequestContext();
if (
Expand Down
10 changes: 8 additions & 2 deletions packages/vinext/src/server/app-rsc-request-normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type NormalizedRscRequest = {
renderMode: AppRscRenderMode;
/** Parsed ClientReuseManifest hint. Verification and skip authorization happen later. */
clientReuseManifest: ClientReuseManifestParseResult;
/** Whether the incoming pathname included the configured basePath. */
hadBasePath: boolean;
};

/**
Expand Down Expand Up @@ -77,6 +79,7 @@ export type NormalizedRscRequest = {
export function normalizeRscRequest(
request: Request,
basePath: string,
allowOutsideBasePath = false,
): Response | NormalizedRscRequest {
const url = new URL(request.url);

Expand All @@ -98,16 +101,18 @@ export function normalizeRscRequest(

// Step 4: Collapse double-slashes and resolve . / .. segments.
let pathname = normalizePath(decoded);
let hadBasePath = true;

// Step 5: basePath check and strip.
// Skipped when basePath is empty (no basePath configured).
// /__vinext/ prefix bypasses the check for internal prerender endpoints
// that must be reachable regardless of basePath configuration.
if (basePath) {
if (!hasBasePath(pathname, basePath) && !pathname.startsWith("/__vinext/")) {
hadBasePath = hasBasePath(pathname, basePath);
if (!hadBasePath && !pathname.startsWith("/__vinext/") && !allowOutsideBasePath) {
return notFoundResponse();
}
pathname = stripBasePath(pathname, basePath);
if (hadBasePath) pathname = stripBasePath(pathname, basePath);
}

// Steps 6-7: RSC detection and cleanPathname.
Expand Down Expand Up @@ -141,6 +146,7 @@ export function normalizeRscRequest(

return {
clientReuseManifest,
hadBasePath,
url,
pathname,
cleanPathname,
Expand Down
Loading
Loading