Skip to content
Draft
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
12 changes: 12 additions & 0 deletions packages/vinext/src/server/edge-runtime-globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AsyncLocalStorage } from "node:async_hooks";

type EdgeRuntimeGlobal = typeof globalThis & {
AsyncLocalStorage?: typeof AsyncLocalStorage;
};

export function installEdgeRuntimeGlobals(target: typeof globalThis = globalThis): void {
const edgeGlobal = target as EdgeRuntimeGlobal;
edgeGlobal.AsyncLocalStorage ??= AsyncLocalStorage;
}

installEdgeRuntimeGlobals();
117 changes: 108 additions & 9 deletions packages/vinext/src/server/pages-api-route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import type { Route } from "../routing/pages-router.js";
import { mergeRouteParamsIntoQuery, parseQueryString } from "../utils/query.js";
import { addQueryParam } from "../utils/query.js";
import { NextRequest } from "vinext/shims/server";
import {
createPagesReqRes,
parsePagesBodySizeLimit,
parsePagesApiBody,
type PagesRequestQuery,
type PagesReqResRequest,
type PagesReqResResponse,
PagesApiBodyParseError,
} from "./pages-node-compat.js";
import { internalServerErrorResponse } from "./http-error-responses.js";

type PagesApiRouteModule = {
default?: (req: PagesReqResRequest, res: PagesReqResResponse) => void | Promise<void>;
config?: {
api?: {
bodyParser?: false | { sizeLimit?: number | string };
};
runtime?: string;
};
runtime?: string;
default?: unknown;
};

export type PagesApiRouteMatch = {
Expand All @@ -23,13 +31,70 @@ export type PagesApiRouteMatch = {

type HandlePagesApiRouteOptions = {
match: PagesApiRouteMatch | null;
onRevalidate?: (
urlPath: string,
options?: { unstable_onlyGenerated?: boolean },
) => Promise<void> | void;
reportRequestError?: (error: Error, routePattern: string) => void | Promise<void>;
request: Request;
url: string;
};

const warnedEdgeRuntimeRoutes = new Set<string>();

function normalizeEdgeRuntimeResponse(response: Response): Response {
if (!response.headers.has("content-encoding") && !response.headers.has("content-length")) {
return response;
}

const headers = new Headers(response.headers);
// Node's fetch decodes compressed upstream bodies but keeps the original
// encoding headers. The deploy harness runs Pages edge routes in Node, so
// strip body metadata before the production server optionally recompresses.
headers.delete("content-encoding");
headers.delete("content-length");

return new Response(response.body, {
headers,
status: response.status,
statusText: response.statusText,
});
}

function buildPagesApiQuery(url: string, params: PagesRequestQuery): PagesRequestQuery {
return mergeRouteParamsIntoQuery(parseQueryString(url), params);
const query: PagesRequestQuery = { ...params };
const search = url.split("?")[1];
if (!search) {
return query;
}

for (const [key, value] of new URLSearchParams(search)) {
addQueryParam(query, key, value);
}

return query;
}

function appendRouteParams(searchParams: URLSearchParams, params: PagesRequestQuery): void {
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
for (const item of value) {
searchParams.append(key, item);
}
} else {
searchParams.append(key, value);
}
}
}

function requestWithResolvedUrl(
request: Request,
url: string,
params: PagesRequestQuery = {},
): Request {
const resolvedUrl = new URL(url, request.url);
appendRouteParams(resolvedUrl.searchParams, params);
return new Request(resolvedUrl, request);
}

export async function handlePagesApiRoute(options: HandlePagesApiRouteOptions): Promise<Response> {
Expand All @@ -44,17 +109,51 @@ export async function handlePagesApiRoute(options: HandlePagesApiRouteOptions):
}

try {
const runtime = route.module.config?.runtime ?? route.module.runtime;
if (runtime === "edge" || runtime === "experimental-edge") {
if (!warnedEdgeRuntimeRoutes.has(route.pattern)) {
warnedEdgeRuntimeRoutes.add(route.pattern);
console.warn(
`[vinext] Pages API route ${route.pattern} exports config.runtime = "edge". ` +
"vinext does not implement Next.js Edge Runtime isolation; this route will run " +
"as a best-effort Web Request/Response handler on the normal vinext runtime. " +
"Prefer the default Node.js Pages API runtime, or migrate request-boundary logic " +
"to proxy.ts. See https://nextjs.org/blog/next-16#proxyts-formerly-middlewarets",
);
}
const resolvedRequest = requestWithResolvedUrl(options.request, options.url, params);
const edgeRequest =
resolvedRequest instanceof NextRequest ? resolvedRequest : new NextRequest(resolvedRequest);
const edgeHandler = handler as (req: NextRequest) => unknown;
const result = await edgeHandler(edgeRequest);
if (result instanceof Response) {
return normalizeEdgeRuntimeResponse(result);
}
return new Response(null, { status: 204 });
}

const query = buildPagesApiQuery(options.url, params);
const body = await parsePagesApiBody(options.request);
const { req, res, responsePromise } = createPagesReqRes({
const apiConfig = route.module.config?.api;
const shouldParseBody = apiConfig?.bodyParser !== false;
const sizeLimit =
shouldParseBody && typeof apiConfig?.bodyParser === "object"
? parsePagesBodySizeLimit(apiConfig.bodyParser.sizeLimit)
: undefined;
const body = shouldParseBody ? await parsePagesApiBody(options.request, sizeLimit) : undefined;
const { isResponsePiped, req, res, responsePromise } = createPagesReqRes({
body,
onRevalidate: options.onRevalidate,
preserveRequestBodyStream: !shouldParseBody,
query,
request: options.request,
url: options.url,
});

await handler(req, res);
res.end();
const nodeHandler = handler as (req: PagesReqResRequest, res: PagesReqResResponse) => unknown;
await nodeHandler(req, res);
if (!res.headersSent && !isResponsePiped()) {
res.end();
}
return await responsePromise;
} catch (error) {
if (error instanceof PagesApiBodyParseError) {
Expand All @@ -68,6 +167,6 @@ export async function handlePagesApiRoute(options: HandlePagesApiRouteOptions):
error instanceof Error ? error : new Error(String(error)),
route.pattern,
);
return internalServerErrorResponse();
return new Response("Internal Server Error", { status: 500 });
}
}
Loading
Loading