Skip to content
Open
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: 3 additions & 0 deletions examples/app-router-cloudflare/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function AdminPage() {
return <main>Protected admin content</main>;
}
5 changes: 4 additions & 1 deletion examples/app-router-cloudflare/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { NextRequest, NextResponse } from "next/server";
* Middleware for app-router-cloudflare example.
*/
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/admin") {
return new Response("Blocked by middleware", { status: 403 });
}
if (request.nextUrl.pathname === "/_next/static/middleware-rewrite.js") {
return new Response("rewritten missing asset", {
headers: { "content-type": "text/plain" },
Expand All @@ -22,5 +25,5 @@ export function middleware(request: NextRequest) {
}

export const config = {
matcher: ["/api/:path*", "/", "/_next/static/middleware-rewrite.js"],
matcher: ["/api/:path*", "/", "/admin", "/_next/static/middleware-rewrite.js"],
};
31 changes: 22 additions & 9 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,10 @@ function matchRoute(url) {
return __routeMatcher.matchRoute(url);
}

function matchRequestRoute(url) {
return __routeMatcher.matchRequestRoute(url);
}

/**
* Check if a pathname matches any intercepting route.
* Returns the match info or null.
Expand Down Expand Up @@ -737,6 +741,7 @@ export default createAppRscHandler({
actionFailed,
handlerStart,
interceptionContext,
interceptionPathname,
isProgressiveActionRender,
isRscRequest,
middlewareContext,
Expand Down Expand Up @@ -821,7 +826,10 @@ export default createAppRscHandler({
fetchCache: __segmentConfig.fetchCache ?? null,
isEdgeRuntime: __isEdgeRuntime(__segmentConfig.runtime),
findIntercept(pathname) {
return findIntercept(pathname, interceptionContext);
return findIntercept(
pathname === cleanPathname ? interceptionPathname : pathname,
interceptionContext,
);
},
generateStaticParams: __generateStaticParams,
getFontLinks: _getSSRFontLinks,
Expand Down Expand Up @@ -873,7 +881,7 @@ export default createAppRscHandler({
});
},
async probePage(probeSearchParams = searchParams) {
const __probeIntercept = findIntercept(cleanPathname, interceptionContext);
const __probeIntercept = findIntercept(interceptionPathname, interceptionContext);
// The intercepting-route page module is lazy (page: null + __pageLoader).
// Resolve it before probing so buildAppPageProbes inspects the real page
// component for dynamic bailout — matching the render path, which also
Expand All @@ -895,7 +903,7 @@ export default createAppRscHandler({
}));
},
renderErrorBoundaryPage(renderErr, errorOrigin) {
const __activeIntercept = findIntercept(cleanPathname, interceptionContext);
const __activeIntercept = findIntercept(interceptionPathname, interceptionContext);
return __fallbackRenderer.renderErrorBoundary(route, renderErr, isRscRequest, request, params, scriptNonce, middlewareContext, {
isEdgeRuntime: __isEdgeRuntime(__segmentConfig.runtime),
sourcePageSegments: __activeIntercept?.slotKey === __SIBLING_PAGE_INTERCEPT_SLOT_KEY
Expand All @@ -904,7 +912,7 @@ export default createAppRscHandler({
}, errorOrigin);
},
renderHttpAccessFallbackPage(statusCode, opts, currentMiddlewareContext) {
const __activeIntercept = findIntercept(cleanPathname, interceptionContext);
const __activeIntercept = findIntercept(interceptionPathname, interceptionContext);
return __fallbackRenderer.renderHttpAccessFallback(route, statusCode, isRscRequest, request, opts, scriptNonce, currentMiddlewareContext, {
isEdgeRuntime: __isEdgeRuntime(__segmentConfig.runtime),
sourcePageSegments: __activeIntercept?.slotKey === __SIBLING_PAGE_INTERCEPT_SLOT_KEY
Expand Down Expand Up @@ -991,6 +999,7 @@ export default createAppRscHandler({
contentType,
middlewareContext,
request,
routeMatch,
}) {
const {
handleProgressiveServerActionRequest: __handleProgressiveServerActionRequest,
Expand All @@ -1011,11 +1020,10 @@ export default createAppRscHandler({
contentType,
actionId,
);
const __progressiveActionMatch = __isProgressiveAction ? matchRoute(cleanPathname) : null;
const __hasPageRoute = Boolean(
__progressiveActionMatch &&
__progressiveActionMatch.route.__loadPage &&
!__progressiveActionMatch.route.__loadRouteHandler,
__isProgressiveAction &&
routeMatch?.route.__loadPage &&
!routeMatch.route.__loadRouteHandler,
);
return __handleProgressiveServerActionRequest({
actionId,
Expand Down Expand Up @@ -1048,14 +1056,16 @@ export default createAppRscHandler({
middlewareContext,
mountedSlotsHeader,
request,
routeMatch,
routePathname,
searchParams,
}) {
const {
handleServerActionRscRequest: __handleServerActionRscRequest,
readActionBodyWithLimit: __readBodyWithLimit,
readActionFormDataWithLimit: __readFormDataWithLimit,
} = await __loadAppServerActionExecution();
const __actionMatch = matchRoute(cleanPathname);
const __actionMatch = routeMatch;
if (__actionMatch) await __ensureRouteLoaded(__actionMatch.route);
const __actionIsEdgeRuntime = __actionMatch
? __isEdgeRuntime(__resolveRouteRuntime(__actionMatch.route))
Expand Down Expand Up @@ -1095,6 +1105,8 @@ export default createAppRscHandler({
__clearRequestContext();
},
contentType,
currentRouteMatch: __actionMatch,
currentRoutePathname: routePathname,
createNotFoundElement(actionRouteId) {
return {
...__AppElementsWire.createMetadataEntries({
Expand Down Expand Up @@ -1188,6 +1200,7 @@ export default createAppRscHandler({
: ""
}
matchRoute,
matchRequestRoute,
${
middlewarePath
? `runMiddleware({ cleanPathname, context, hadBasePath, isDataRequest, request }) {
Expand Down
2 changes: 2 additions & 0 deletions packages/vinext/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@ declare module "node:http" {
* the final response status.
*/
__vinextMiddlewareStatus?: number;
/** Encoded request URL captured before Vite normalizes the pathname. */
__vinextOriginalEncodedUrl?: string;
}
}

Expand Down
20 changes: 19 additions & 1 deletion packages/vinext/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4074,6 +4074,11 @@ export const loadServerActionClient = ${
},

configureServer(server: ViteDevServer) {
server.middlewares.use((req, _res, next) => {
req.__vinextOriginalEncodedUrl ??= req.url;
next();
});

// Watch route files for additions/removals to invalidate route cache.
const pageExtensions = fileMatcher.extensionRegex;

Expand Down Expand Up @@ -4710,6 +4715,10 @@ export const loadServerActionClient = ${
url = url.replace(/\.html(?=\?|$)/, "");
}

// Preserve the original request URL for NextRequest. Vite may
// rewrite extensionless paths to `.html`, while an actual
// `.html` request must remain distinguishable to middleware.
let middlewareUrl = req.__vinextOriginalEncodedUrl ?? url;
let pathname = url.split("?")[0];

// Guard against protocol-relative URL open redirects.
Expand Down Expand Up @@ -4759,6 +4768,13 @@ export const loadServerActionClient = ${
const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
url = stripped + qs;
pathname = stripped;
const middlewarePathname = middlewareUrl.split("?")[0];
if (middlewarePathname.startsWith(bp)) {
const middlewareQs = middlewareUrl.includes("?")
? middlewareUrl.slice(middlewareUrl.indexOf("?"))
: "";
middlewareUrl = (middlewarePathname.slice(bp.length) || "/") + middlewareQs;
}
}

if (nextConfig) {
Expand Down Expand Up @@ -4813,6 +4829,7 @@ export const loadServerActionClient = ${
capturedMiddlewarePath !== null && nextConfig?.trailingSlash === true,
);
url = pagePathname + qs;
middlewareUrl = url;
pathname = pagePathname;
// Rewrite req.url so downstream middleware sees the page
// path, not the raw _next/data URL.
Expand Down Expand Up @@ -4947,7 +4964,7 @@ export const loadServerActionClient = ${
const mwProto =
rawProto === "https" || rawProto === "http" ? rawProto : "http";
const mwOrigin = `${mwProto}://${requestHost}`;
const middlewareRequest = new Request(new URL(url, mwOrigin), {
const middlewareRequest = new Request(new URL(middlewareUrl, mwOrigin), {
method: req.method,
headers: nodeRequestHeaders,
});
Expand All @@ -4959,6 +4976,7 @@ export const loadServerActionClient = ${
nextConfig?.basePath,
nextConfig?.trailingSlash,
opts.isDataRequest,
pathname,
);

// Forward middleware context to the RSC entry so it can
Expand Down
13 changes: 10 additions & 3 deletions packages/vinext/src/routing/route-pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export function fillRoutePatternSegments(
export function matchRoutePattern(
urlParts: readonly string[],
patternParts: readonly string[],
): RoutePatternParams | null {
const params = matchRoutePatternRaw(urlParts, patternParts);
if (params) decodeMatchedParams(params);
return params;
}

export function matchRoutePatternRaw(
urlParts: readonly string[],
patternParts: readonly string[],
): RoutePatternParams | null {
const params: RoutePatternParams = Object.create(null);

Expand Down Expand Up @@ -137,9 +146,7 @@ export function matchRoutePattern(
return matchFrom(urlIndex + 1, patternIndex + 1);
}

if (!matchFrom(0, 0)) return null;
decodeMatchedParams(params);
return params;
return matchFrom(0, 0) ? params : null;
}

export function matchRoutePatternPrefix(
Expand Down
9 changes: 8 additions & 1 deletion packages/vinext/src/routing/route-trie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,20 @@ export function trieMatch<R>(
root: TrieNode<R>,
urlParts: string[],
): { route: R; params: Record<string, string | string[]> } | null {
const result = match(root, urlParts, 0, []);
const result = trieMatchRaw(root, urlParts);
if (result) {
decodeMatchedParams(result.params);
}
return result;
}

export function trieMatchRaw<R>(
root: TrieNode<R>,
urlParts: string[],
): { route: R; params: Record<string, string | string[]> } | null {
return match(root, urlParts, 0, []);
}

function match<R>(
node: TrieNode<R>,
urlParts: string[],
Expand Down
11 changes: 9 additions & 2 deletions packages/vinext/src/server/app-page-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,13 @@ function areStaticParamsAllowed(
staticParams: readonly Record<string, unknown>[],
): boolean {
const paramKeys = Object.keys(params);
const stringParamMatches = (value: string, staticValue: string): boolean => {
const normalizedValue = value.toLowerCase();
return (
normalizedValue === staticValue.toLowerCase() ||
normalizedValue === encodeURIComponent(staticValue).toLowerCase()
);
};

return staticParams.some((staticParamSet) =>
paramKeys.every((key) => {
Expand All @@ -372,14 +379,14 @@ function areStaticParamsAllowed(
value.length === staticValue.length &&
value.every((part, index) =>
typeof staticValue[index] === "string"
? part.toLowerCase() === staticValue[index].toLowerCase()
? stringParamMatches(part, staticValue[index])
: part === staticValue[index],
)
);
}

if (typeof staticValue === "string") {
return value.toLowerCase() === staticValue.toLowerCase();
return stringParamMatches(value, staticValue);
}

if (typeof staticValue === "number" || typeof staticValue === "boolean") {
Expand Down
Loading
Loading