Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5fdfb94
autoresearch: setup compat test audit loop
dmmulroy Mar 17, 2026
5b5b084
Baseline: 233 passing compat tests, 21 files, 13 dirs covered. Fixed …
dmmulroy Mar 17, 2026
129b6ed
Port app-fetch-deduping-errors: 3 tests for page rendering despite fe…
dmmulroy Mar 17, 2026
9342d34
Port forbidden: 5 tests for forbidden() boundary, 403 status, scoped …
dmmulroy Mar 17, 2026
17473c7
Port unauthorized: 5 tests for unauthorized() boundary, 401 status, s…
dmmulroy Mar 17, 2026
59bd8bc
Port catch-all-optional: 3 tests for optional catch-all routing. Batc…
dmmulroy Mar 17, 2026
5a03d6d
Port simple-routes + FIX BUG: route handlers received plain Request i…
dmmulroy Mar 17, 2026
3979dbe
Port rsc-redirect: 3 tests for redirect() from server component. Foun…
dmmulroy Mar 17, 2026
bff4433
Port static-generation-status: 5 tests for notFound/redirect/permanen…
dmmulroy Mar 17, 2026
baf1790
Port layout-params: 6 tests. BUG FOUND+FIXED: layouts received ALL pa…
dmmulroy Mar 17, 2026
799d950
Port metadata-dynamic-routes: 10 tests for robots.txt, sitemap.xml (a…
dmmulroy Mar 17, 2026
8a32893
Port searchparams-static-bailout: 5 tests for searchParams in server/…
dmmulroy Mar 17, 2026
3ebe0e4
Port use-params (3 tests: useParams SSR for single/nested/catchall) +…
dmmulroy Mar 17, 2026
bee5680
Port _allow-underscored-root-directory: 3 tests using dedicated fixtu…
dmmulroy Mar 17, 2026
3675a4b
Port use-cache-route-handler-only: 2 tests using a dedicated route-ha…
dmmulroy Mar 17, 2026
b016176
Port dynamic-data: 4 dev-mode HTTP tests for top-level, force-dynamic…
dmmulroy Mar 17, 2026
070e7d9
Port rewrites-redirects: 2 pure-HTTP tests for exotic URL-scheme redi…
dmmulroy Mar 17, 2026
0efb654
Fix benchmark harness: autoresearch.sh now checks Vitest exit code in…
dmmulroy Mar 17, 2026
fbfb261
Port app-routes-trailing-slash: 2 tests using dedicated trailingSlash…
dmmulroy Mar 17, 2026
9fb39a9
Port not-found-default HTTP subset (3 tests) with a dedicated fixture…
dmmulroy Mar 17, 2026
9a36356
Port partial app-middleware coverage (+11 HTTP tests) with a dedicate…
dmmulroy Mar 17, 2026
1063790
chore: drop autoresearch assets from PR branch
dmmulroy Mar 17, 2026
fe7374a
test: add draft-mode middleware edge cases and root optional catch-al…
dmmulroy Mar 17, 2026
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/react-dom": "catalog:",
"@typescript/native-preview": "catalog:",
"@vitejs/plugin-rsc": "catalog:",
"cheerio": "^1.2.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: ^1.2.0 is a wide range for a test utility. Consider pinning to 1.2.0 to avoid surprise breakage from semver-minor bumps, though this is very low risk for a test-only dep.

"image-size": "catalog:",
"next": "catalog:",
"playwright": "catalog:",
Expand Down
114 changes: 99 additions & 15 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,9 @@ function renderToReadableStream(model, options) {
}
import { createElement, Suspense, Fragment } from "react";
import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, applyMiddlewareRequestHeaders, getHeadersContext, setHeadersAccessPhase } from "next/headers";
import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, applyMiddlewareRequestHeaders, getHeadersContext, setHeadersAccessPhase, runWithHeadersContext } from "next/headers";
import { NextRequest, NextFetchEvent } from "next/server";
import DefaultHttpErrorComponent from "next/error";
import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
import { LayoutSegmentProvider } from "vinext/layout-segment-context";
import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
Expand Down Expand Up @@ -509,6 +510,36 @@ function makeThenableParams(obj) {
return Object.assign(Promise.resolve(plain), plain);
}

// Compute the subset of params that a layout at a given tree position should receive.
// In Next.js, each layout only sees params from its own segment and ancestor segments —
// NOT from child segments deeper in the tree. For example, with
// /base/[param1]/[param2]/page.tsx, the layout at [param1]/ gets {param1} but not {param2}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is well-structured and the loop bounds are correct. Verified: treePosition = directory depth from appDir, so for (i = 0; i < treePosition) correctly includes the layout's own dynamic segment.

Minor observation: the segment-type detection logic (optional catch-all, catch-all, dynamic) is duplicated between __scopeParamsForLayout and __resolveChildSegments. Not worth extracting now since the patterns are small and stable, but flagging it in case this grows.

function __scopeParamsForLayout(routeSegments, treePosition, fullParams) {
var scoped = {};
// Scan segments from root up to (but not including) this layout's position
for (var i = 0; i < treePosition; i++) {
var seg = routeSegments[i];
if (!seg) continue;
// Optional catch-all: [[...param]]
if (seg.indexOf("[[...") === 0 && seg.charAt(seg.length - 1) === "]" && seg.charAt(seg.length - 2) === "]") {
var pn = seg.slice(5, -2);
// Only include if the value is a non-empty array (Next.js omits empty optional catch-all)
if (pn in fullParams && Array.isArray(fullParams[pn]) && fullParams[pn].length > 0) scoped[pn] = fullParams[pn];
// Catch-all: [...param]
} else if (seg.indexOf("[...") === 0 && seg.charAt(seg.length - 1) === "]") {
var pn2 = seg.slice(4, -1);
if (pn2 in fullParams) scoped[pn2] = fullParams[pn2];
// Dynamic: [param]
// The dot guard is defensive — routing currently only produces [w-]+ dynamic
// segments, but it prevents accidental matches on any future segment formats.
} else if (seg.charAt(0) === "[" && seg.charAt(seg.length - 1) === "]" && seg.indexOf(".") === -1) {
var pn3 = seg.slice(1, -1);
if (pn3 in fullParams) scoped[pn3] = fullParams[pn3];
}
}
return scoped;
}

// Resolve route tree segments to actual values using matched params.
// Dynamic segments like [id] are replaced with param values, catch-all
// segments like [...slug] are joined with "/", and route groups are kept as-is.
Expand All @@ -531,6 +562,8 @@ function __resolveChildSegments(routeSegments, treePosition, params) {
var v2 = params[pn2];
result.push(Array.isArray(v2) ? v2.join("/") : (v2 || seg));
// Dynamic: [param]
// The dot guard is defensive — routing currently only produces [w-]+ dynamic
// segments, but it prevents accidental matches on any future segment formats.
} else if (seg.charAt(0) === "[" && seg.charAt(seg.length - 1) === "]" && seg.indexOf(".") === -1) {
var pn3 = seg.slice(1, -1);
result.push(params[pn3] || seg);
Expand Down Expand Up @@ -742,6 +775,11 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
BoundaryComponent = boundaryModule?.default ?? null;
}
const layouts = opts?.layouts ?? route?.layouts ?? rootLayouts;
if (!BoundaryComponent && statusCode === 404) {
BoundaryComponent = function DefaultNotFoundBoundary() {
return createElement(DefaultHttpErrorComponent, { statusCode: 404 });
};
}
if (!BoundaryComponent) return null;

// Resolve metadata and viewport from parent layouts so that not-found/error
Expand Down Expand Up @@ -791,12 +829,13 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
const _treePositions = route?.layoutTreePositions;
const _routeSegs = route?.routeSegments || [];
const _fallbackParams = opts?.matchedParams ?? route?.params ?? {};
const _asyncFallbackParams = makeThenableParams(_fallbackParams);
for (let i = layouts.length - 1; i >= 0; i--) {
const LayoutComponent = layouts[i]?.default;
if (LayoutComponent) {
element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParams });
const _tp = _treePositions ? _treePositions[i] : 0;
const _scopedParams = __scopeParamsForLayout(_routeSegs, _tp, _fallbackParams);
const _asyncScopedParams = makeThenableParams(_scopedParams);
element = createElement(LayoutComponent, { children: element, params: _asyncScopedParams });
const _cs = __resolveChildSegments(_routeSegs, _tp, _fallbackParams);
element = createElement(LayoutSegmentProvider, { childSegments: _cs }, element);
}
Expand Down Expand Up @@ -835,11 +874,15 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
// For HTML (full page load) responses, wrap with layouts only (no client-side
// wrappers needed since SSR generates the complete HTML document).
const _fallbackParamsHtml = opts?.matchedParams ?? route?.params ?? {};
const _asyncFallbackParamsHtml = makeThenableParams(_fallbackParamsHtml);
const _treePositionsHtml = route?.layoutTreePositions;
const _routeSegsHtml = route?.routeSegments || [];
for (let i = layouts.length - 1; i >= 0; i--) {
const LayoutComponent = layouts[i]?.default;
if (LayoutComponent) {
element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParamsHtml });
const _tpHtml = _treePositionsHtml ? _treePositionsHtml[i] : 0;
const _scopedParamsHtml = __scopeParamsForLayout(_routeSegsHtml, _tpHtml, _fallbackParamsHtml);
const _asyncScopedParamsHtml = makeThenableParams(_scopedParamsHtml);
element = createElement(LayoutComponent, { children: element, params: _asyncScopedParamsHtml });
}
}
const _pathname = new URL(request.url).pathname;
Expand Down Expand Up @@ -930,12 +973,13 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
const _errTreePositions = route?.layoutTreePositions;
const _errRouteSegs = route?.routeSegments || [];
const _errParams = matchedParams ?? route?.params ?? {};
const _asyncErrParams = makeThenableParams(_errParams);
for (let i = layouts.length - 1; i >= 0; i--) {
const LayoutComponent = layouts[i]?.default;
if (LayoutComponent) {
element = createElement(LayoutComponent, { children: element, params: _asyncErrParams });
const _etp = _errTreePositions ? _errTreePositions[i] : 0;
const _errScopedParams = __scopeParamsForLayout(_errRouteSegs, _etp, _errParams);
const _asyncErrScopedParams = makeThenableParams(_errScopedParams);
element = createElement(LayoutComponent, { children: element, params: _asyncErrScopedParams });
const _ecs = __resolveChildSegments(_errRouteSegs, _etp, _errParams);
element = createElement(LayoutSegmentProvider, { childSegments: _ecs }, element);
}
Expand All @@ -956,11 +1000,15 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
} else {
// For HTML (full page load) responses, wrap with layouts only.
const _errParamsHtml = matchedParams ?? route?.params ?? {};
const _asyncErrParamsHtml = makeThenableParams(_errParamsHtml);
const _errTreePositionsHtml = route?.layoutTreePositions;
const _errRouteSegsHtml = route?.routeSegments || [];
for (let i = layouts.length - 1; i >= 0; i--) {
const LayoutComponent = layouts[i]?.default;
if (LayoutComponent) {
element = createElement(LayoutComponent, { children: element, params: _asyncErrParamsHtml });
const _etpHtml = _errTreePositionsHtml ? _errTreePositionsHtml[i] : 0;
const _errScopedParamsHtml = __scopeParamsForLayout(_errRouteSegsHtml, _etpHtml, _errParamsHtml);
const _asyncErrScopedParamsHtml = makeThenableParams(_errScopedParamsHtml);
element = createElement(LayoutComponent, { children: element, params: _asyncErrScopedParamsHtml });
}
}
}
Expand Down Expand Up @@ -1154,10 +1202,17 @@ async function buildPageElement(route, params, opts, searchParams) {
});
}

// force-static pages receive empty searchParams rather than real request data.
// This mirrors Next.js, which strips dynamic request state from force-static
// render paths instead of exposing live values.
const isForceStatic = route.page?.dynamic === "force-static";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix. The effectiveSpObj / effectiveHasSearchParams pattern is clean — both metadata resolution and page props consistently see the empty state for force-static pages.

const effectiveSpObj = isForceStatic ? {} : spObj;
const effectiveHasSearchParams = isForceStatic ? false : hasSearchParams;

const [layoutMetaResults, layoutVpResults, pageMeta, pageVp] = await Promise.all([
Promise.all(layoutMetaPromises),
Promise.all(layoutMods.map((mod) => resolveModuleViewport(mod, params).catch((err) => { console.error("[vinext] Layout generateViewport() failed:", err); return null; }))),
route.page ? resolveModuleMetadata(route.page, params, spObj, pageParentPromise) : Promise.resolve(null),
route.page ? resolveModuleMetadata(route.page, params, effectiveSpObj, pageParentPromise) : Promise.resolve(null),
route.page ? resolveModuleViewport(route.page, params) : Promise.resolve(null),
]);

Expand All @@ -1176,7 +1231,7 @@ async function buildPageElement(route, params, opts, searchParams) {
// Always provide searchParams prop when the URL object is available, even
// when the query string is empty -- pages that do "await searchParams" need
// it to be a thenable rather than undefined.
pageProps.searchParams = makeThenableParams(spObj);
pageProps.searchParams = makeThenableParams(effectiveSpObj);
// If the URL has query parameters, mark the page as dynamic.
// In Next.js, only accessing the searchParams prop signals dynamic usage,
// but a Proxy-based approach doesn't work here because React's RSC debug
Expand All @@ -1185,7 +1240,7 @@ async function buildPageElement(route, params, opts, searchParams) {
// read searchParams. Checking for non-empty query params is a safe
// approximation: pages with query params in the URL are almost always
// dynamic, and this avoids false positives from React internals.
if (hasSearchParams) markDynamicUsage();
if (effectiveHasSearchParams) markDynamicUsage();
}
let element = createElement(PageComponent, pageProps);

Expand Down Expand Up @@ -1288,7 +1343,11 @@ async function buildPageElement(route, params, opts, searchParams) {
}
}

const layoutProps = { children: element, params: makeThenableParams(params) };
// Scope params for this layout — each layout only sees params from its
// own segment and ancestor segments, not child dynamic segments.
const _bpeTp = route.layoutTreePositions ? route.layoutTreePositions[i] : 0;
const _bpeScopedParams = __scopeParamsForLayout(route.routeSegments || [], _bpeTp, params);
const layoutProps = { children: element, params: makeThenableParams(_bpeScopedParams) };

// Add parallel slot elements to the layout that defines them.
// Each slot has a layoutIndex indicating which layout it belongs to.
Expand Down Expand Up @@ -1778,7 +1837,29 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
const mwRequest = new Request(mwUrl, request);
const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
const mwFetchEvent = new NextFetchEvent({ page: cleanPathname });
const mwResponse = await middlewareFn(nextRequest, mwFetchEvent);
// SYNC: middleware-headers-context — this pattern is duplicated in
// server/middleware.ts and entries/pages-server-entry.ts.
// Changes here must be mirrored in both sibling files.
let _mwDraftCookie = null;
let mwResponse = await runWithHeadersContext(headersContextFromRequest(nextRequest), async () => {
const _prevHeadersPhase = setHeadersAccessPhase("middleware");
try {
const _middlewareResponse = await middlewareFn(nextRequest, mwFetchEvent);
_mwDraftCookie = getDraftModeCookieHeader();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: runWithHeadersContext always initializes phase: "render" (see headers.ts line 224), which is immediately overridden by setHeadersAccessPhase("middleware") on the next line. This works fine but the initial phase set is wasted. Very minor.

return _middlewareResponse;
} finally {
setHeadersAccessPhase(_prevHeadersPhase);
}
});
if (_mwDraftCookie && mwResponse) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capturing getDraftModeCookieHeader() inside the runWithHeadersContext scope (before it unwinds) is critical — good catch. The draft mode cookie would be lost if captured after the ALS scope unwinds.

const _mwHeaders = new Headers(mwResponse.headers);
_mwHeaders.append("set-cookie", _mwDraftCookie);
mwResponse = new Response(mwResponse.body, {
status: mwResponse.status,
statusText: mwResponse.statusText,
headers: _mwHeaders,
});
}
const _mwWaitUntil = mwFetchEvent.drainWaitUntil();
const _mwExecCtx = _getRequestExecutionContext();
if (_mwExecCtx && typeof _mwExecCtx.waitUntil === "function") { _mwExecCtx.waitUntil(_mwWaitUntil); }
Expand Down Expand Up @@ -2336,7 +2417,10 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
if (typeof handlerFn === "function") {
const previousHeadersPhase = setHeadersAccessPhase("route-handler");
try {
const response = await handlerFn(request, { params });
// Wrap in NextRequest so route handlers get .nextUrl, .cookies, .geo, .ip, etc.
// Next.js passes NextRequest to route handlers, not plain Request.
const routeRequest = request instanceof NextRequest ? request : new NextRequest(request);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix. Note that unlike page components which receive makeThenableParams(params), route handlers receive plain params. This is actually correct — Next.js route handlers receive plain params objects, not Promise-wrapped ones.

const response = await handlerFn(routeRequest, { params });
const dynamicUsedInHandler = consumeDynamicUsage();
const handlerSetCacheControl = response.headers.has("cache-control");

Expand Down
28 changes: 26 additions & 2 deletions packages/vinext/src/entries/pages-server-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ if (typeof _instrumentation.onRequestError === "function") {
// Generate middleware code if middleware.ts exists
const middlewareImportCode = middlewarePath
? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};
import { NextRequest, NextFetchEvent } from "next/server";`
import { NextRequest, NextFetchEvent } from "next/server";
import { getDraftModeCookieHeader, headersContextFromRequest, runWithHeadersContext, setHeadersAccessPhase } from "next/headers";`
: "";

// The matcher config is read from the middleware module at import time.
Expand Down Expand Up @@ -198,12 +199,35 @@ async function _runMiddleware(request) {
}
var nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
var fetchEvent = new NextFetchEvent({ page: normalizedPathname });
// SYNC: middleware-headers-context — this pattern is duplicated in
// server/middleware.ts and entries/app-rsc-entry.ts.
// Changes here must be mirrored in both sibling files.
var response;
try { response = await middlewareFn(nextRequest, fetchEvent); }
var draftCookie = null;
try {
response = await runWithHeadersContext(headersContextFromRequest(nextRequest), async function() {
var previousPhase = setHeadersAccessPhase("middleware");
try {
var middlewareResponse = await middlewareFn(nextRequest, fetchEvent);
draftCookie = getDraftModeCookieHeader();
return middlewareResponse;
}
finally { setHeadersAccessPhase(previousPhase); }
});
}
catch (e) {
console.error("[vinext] Middleware error:", e);
return { continue: false, response: new Response("Internal Server Error", { status: 500 }) };
}
if (draftCookie && response) {
var responseHeadersWithDraft = new Headers(response.headers);
responseHeadersWithDraft.append("set-cookie", draftCookie);
response = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: responseHeadersWithDraft,
});
}
var _mwCtx = _getRequestExecutionContext();
if (_mwCtx && typeof _mwCtx.waitUntil === "function") { _mwCtx.waitUntil(fetchEvent.drainWaitUntil()); } else { fetchEvent.drainWaitUntil(); }

Expand Down
35 changes: 33 additions & 2 deletions packages/vinext/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import {
} from "../config/config-matchers.js";
import type { HasCondition, NextI18nConfig } from "../config/next-config.js";
import { NextRequest, NextFetchEvent } from "../shims/server.js";
import {
getDraftModeCookieHeader,
headersContextFromRequest,
runWithHeadersContext,
setHeadersAccessPhase,
} from "../shims/headers.js";
import { normalizePath } from "./normalize-path.js";
import { shouldKeepMiddlewareHeader } from "./middleware-request-headers.js";
import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
Expand Down Expand Up @@ -438,10 +444,25 @@ export async function runMiddleware(
const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
const fetchEvent = new NextFetchEvent({ page: normalizedPathname });

// Execute the middleware
// SYNC: middleware-headers-context — this pattern is duplicated in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SYNC tag middleware-headers-context is a good practice for keeping the three copies in sync. I confirmed the pattern is consistently applied across all three files (server/middleware.ts, entries/app-rsc-entry.ts, entries/pages-server-entry.ts).

// entries/pages-server-entry.ts and entries/app-rsc-entry.ts (code-generated).
// Changes here must be mirrored in both entry templates.
//
// Execute the middleware with a next/headers context so middleware can use
// headers() / draftMode() like Next.js allows.
let response: Response | undefined;
let draftCookie: string | null = null;
try {
response = await middlewareFn(nextRequest, fetchEvent);
response = await runWithHeadersContext(headersContextFromRequest(nextRequest), async () => {
const previousPhase = setHeadersAccessPhase("middleware");
try {
const middlewareResponse = await middlewareFn(nextRequest, fetchEvent);
draftCookie = getDraftModeCookieHeader();
return middlewareResponse;
} finally {
setHeadersAccessPhase(previousPhase);
}
});
} catch (e: any) {
console.error("[vinext] Middleware error:", e);
const message =
Expand All @@ -456,6 +477,16 @@ export async function runMiddleware(
};
}

if (draftCookie && response) {
const responseHeaders = new Headers(response.headers);
responseHeaders.append("set-cookie", draftCookie);
response = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
}

// Drain waitUntil promises (fire-and-forget: we don't block the response
// on these — matches platform semantics where waitUntil runs after response).
void fetchEvent.drainWaitUntil();
Expand Down
2 changes: 1 addition & 1 deletion packages/vinext/src/shims/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface HeadersContext {
readonlyHeaders?: Headers;
}

export type HeadersAccessPhase = "render" | "action" | "route-handler";
export type HeadersAccessPhase = "render" | "action" | "route-handler" | "middleware";

export type VinextHeadersShimState = {
headersContext: HeadersContext | null;
Expand Down
Loading
Loading