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
5 changes: 2 additions & 3 deletions packages/aws/src/overrides/converters/aws-apigw-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import {
} from "@opennextjs/core/overrides/converters/utils.js";
import type { InternalEvent, InternalResult } from "@opennextjs/core/types/open-next.js";
import type { Converter } from "@opennextjs/core/types/overrides.js";
import { fromReadableStream } from "@opennextjs/core/utils/stream.js";
import { fromReadableStream, toReadableStream } from "@opennextjs/core/utils/stream.js";
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

function normalizeAPIGatewayProxyEventHeaders(event: APIGatewayProxyEvent): Record<string, string> {
event.multiValueHeaders;
const headers: Record<string, string> = {};

for (const [key, values] of Object.entries(event.multiValueHeaders || {})) {
Expand Down Expand Up @@ -73,7 +72,7 @@ async function convertFromAPIGatewayProxyEvent(event: APIGatewayProxyEvent): Pro
method: httpMethod,
rawPath: path,
url: `https://${extractHostFromHeaders(headers)}${path}${normalizeAPIGatewayProxyEventQueryParams(event)}`,
body: Buffer.from(body ?? "", isBase64Encoded ? "base64" : "utf8"),
body: body ? toReadableStream(body, isBase64Encoded) : undefined,
headers,
remoteAddress: requestContext.identity.sourceIp,
query: removeUndefinedFromQuery(normalizeAPIGatewayProxyEventMultiValueQueryStringParameters(event)),
Expand Down
14 changes: 8 additions & 6 deletions packages/aws/src/overrides/converters/aws-apigw-v2.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ReadableStream } from "node:stream/web";

import { debug } from "@opennextjs/core/adapters/logger.js";
import { convertToQuery } from "@opennextjs/core/core/routing/util.js";
import { parseSetCookieHeader } from "@opennextjs/core/http/util.js";
Expand All @@ -7,7 +9,7 @@ import {
} from "@opennextjs/core/overrides/converters/utils.js";
import type { InternalEvent, InternalResult } from "@opennextjs/core/types/open-next.js";
import type { Converter } from "@opennextjs/core/types/overrides.js";
import { fromReadableStream } from "@opennextjs/core/utils/stream.js";
import { fromReadableStream, toReadableStream } from "@opennextjs/core/utils/stream.js";
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";

// Not sure which one is really needed as this is not documented anywhere but server actions redirect are not working without this,
Expand Down Expand Up @@ -36,18 +38,18 @@ const CloudFrontBlacklistedHeaders = [
"via",
];

function normalizeAPIGatewayProxyEventV2Body(event: APIGatewayProxyEventV2): Buffer {
function normalizeAPIGatewayProxyEventV2Body(event: APIGatewayProxyEventV2): ReadableStream | undefined {
const { body, isBase64Encoded } = event;
if (Buffer.isBuffer(body)) {
return body;
return toReadableStream(body);
}
if (typeof body === "string") {
return Buffer.from(body, isBase64Encoded ? "base64" : "utf8");
return toReadableStream(body, isBase64Encoded);
}
if (typeof body === "object") {
return Buffer.from(JSON.stringify(body));
return toReadableStream(JSON.stringify(body));
}
return Buffer.from("", "utf8");
return undefined;
}

function normalizeAPIGatewayProxyEventV2Headers(event: APIGatewayProxyEventV2): Record<string, string> {
Expand Down
5 changes: 2 additions & 3 deletions packages/aws/src/overrides/converters/aws-cloudfront.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import { parseSetCookieHeader } from "@opennextjs/core/http/util.js";
import { extractHostFromHeaders } from "@opennextjs/core/overrides/converters/utils.js";
import type { InternalEvent, InternalResult, MiddlewareResult } from "@opennextjs/core/types/open-next.js";
import type { Converter } from "@opennextjs/core/types/overrides.js";
import { fromReadableStream } from "@opennextjs/core/utils/stream.js";
import { fromReadableStream, toReadableStream } from "@opennextjs/core/utils/stream.js";
import type {
CloudFrontCustomOrigin,
CloudFrontHeaders,
CloudFrontRequest,
CloudFrontRequestEvent,
CloudFrontRequestResult,
} from "aws-lambda";

const cloudfrontBlacklistedHeaders = [
// Disallowed headers, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-function-restrictions-all.html#function-restrictions-disallowed-headers
"connection",
Expand Down Expand Up @@ -82,7 +81,7 @@ async function convertFromCloudFrontRequestEvent(event: CloudFrontRequestEvent):
method,
rawPath: uri,
url: `https://${extractHostFromHeaders(headers)}${uri}${querystring ? `?${querystring}` : ""}`,
body: Buffer.from(body?.data ?? "", body?.encoding === "base64" ? "base64" : "utf8"),
body: body?.data ? toReadableStream(body.data, body.encoding === "base64") : undefined,
headers,
remoteAddress: clientIp,
query: convertToQuery(querystring),
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core/routing/cacheInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ function createPprPartialResult(
"next-resume": "1",
},
rawPath: localizedPath,
body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"),
body: toReadableStream(cachedValue.meta?.postponed || ""),
},
result: {
type: "core",
Expand Down
212 changes: 113 additions & 99 deletions packages/core/src/core/routing/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function defaultMiddlewareLoader() {
* @param initialSearch the initial query string as it was received in the handler
* @param middlewareLoader Only used for unit test
* @returns `Promise<MiddlewareEvent | InternalResult>`
* @throws When loading or executing middleware fails.
*/
export async function handleMiddleware(
internalEvent: InternalEvent,
Expand All @@ -65,30 +66,34 @@ export async function handleMiddleware(

const middleware = await middlewareLoader();

const result: Response = await middleware.default({
// `geo` is pre Next 15.
geo: {
// The city name is percent-encoded.
// See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37
city: decodeURIComponent(headers["x-open-next-city"]),
country: headers["x-open-next-country"],
region: headers["x-open-next-region"],
latitude: headers["x-open-next-latitude"],
longitude: headers["x-open-next-longitude"],
},
headers,
method: internalEvent.method || "GET",
nextConfig: {
basePath: NextConfig.basePath,
i18n: NextConfig.i18n,
trailingSlash: NextConfig.trailingSlash,
},
url,
body: convertBodyToReadableStream(internalEvent.method, internalEvent.body),
} as unknown as Request);
const statusCode = result.status;

/* Apply override headers from middleware
const [bodyForMiddleware, bodyForForward] = internalEvent.body?.tee() ?? [undefined, undefined];
Comment thread
vicb marked this conversation as resolved.

let bodyWasForwarded = false;
try {
const result = await middleware.default({
// `geo` is pre Next 15.
geo: {
// The city name is percent-encoded.
// See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37
city: decodeURIComponent(headers["x-open-next-city"]),
country: headers["x-open-next-country"],
region: headers["x-open-next-region"],
latitude: headers["x-open-next-latitude"],
longitude: headers["x-open-next-longitude"],
},
headers,
method: internalEvent.method || "GET",
nextConfig: {
basePath: NextConfig.basePath,
i18n: NextConfig.i18n,
trailingSlash: NextConfig.trailingSlash,
},
url,
body: convertBodyToReadableStream(internalEvent.method, bodyForMiddleware),
} as unknown as Request);
const statusCode = result.status;

/* Apply override headers from middleware
NextResponse.next({
request: {
headers: new Headers(request.headers),
Expand All @@ -100,87 +105,96 @@ export async function handleMiddleware(
We can delete `x-middleware-override-headers` and check if the key starts with
x-middleware-request- to set the req headers
*/
const responseHeaders = result.headers as Headers;
const reqHeaders: Record<string, string> = {};
const resHeaders: Record<string, string | string[]> = {};

// These are internal headers used by Next.js, we don't want to expose them to the client
const filteredHeaders = [
"x-middleware-override-headers",
"x-middleware-next",
"x-middleware-rewrite",
// We need to drop `content-encoding` because it will be decoded
"content-encoding",
];

const xMiddlewareKey = "x-middleware-request-";
responseHeaders.forEach((value, key) => {
if (key.startsWith(xMiddlewareKey)) {
const k = key.substring(xMiddlewareKey.length);
reqHeaders[k] = value;
} else {
if (filteredHeaders.includes(key.toLowerCase())) return;
if (key.toLowerCase() === "set-cookie") {
resHeaders[key] = resHeaders[key] ? [...resHeaders[key], value] : [value];
} else if (REDIRECTS.has(statusCode) && key.toLowerCase() === "location") {
resHeaders[key] = normalizeLocationHeader(value, internalEvent.url);
const responseHeaders = result.headers as Headers;
const reqHeaders: Record<string, string> = {};
const resHeaders: Record<string, string | string[]> = {};

// These are internal headers used by Next.js, we don't want to expose them to the client
const filteredHeaders = [
"x-middleware-override-headers",
"x-middleware-next",
"x-middleware-rewrite",
// We need to drop `content-encoding` because it will be decoded
"content-encoding",
];

const xMiddlewareKey = "x-middleware-request-";
responseHeaders.forEach((value, key) => {
if (key.startsWith(xMiddlewareKey)) {
const k = key.substring(xMiddlewareKey.length);
reqHeaders[k] = value;
} else {
resHeaders[key] = value;
if (filteredHeaders.includes(key.toLowerCase())) return;
if (key.toLowerCase() === "set-cookie") {
resHeaders[key] = resHeaders[key] ? [...resHeaders[key], value] : [value];
} else if (REDIRECTS.has(statusCode) && key.toLowerCase() === "location") {
resHeaders[key] = normalizeLocationHeader(value, internalEvent.url);
} else {
resHeaders[key] = value;
}
}
}
});

// If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
// NOTE: the header was added to `req` from above
const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
let isExternalRewrite = false;
let middlewareQuery = internalEvent.query;
let newUrl = internalEvent.url;
if (rewriteUrl) {
newUrl = rewriteUrl;
// If not a string, it should probably throw
if (isExternal(newUrl, internalEvent.headers.host as string)) {
isExternalRewrite = true;
} else {
const rewriteUrlObject = new URL(rewriteUrl);
// Search params from the rewritten URL override the original search params

middlewareQuery = getQueryFromSearchParams(rewriteUrlObject.searchParams);

// We still need to add internal search params to the query string for pages router on older versions of Next.js
if ("__nextDataReq" in internalEvent.query) {
middlewareQuery.__nextDataReq = internalEvent.query.__nextDataReq;
});

// If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
// NOTE: the header was added to `req` from above
const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
let isExternalRewrite = false;
let middlewareQuery = internalEvent.query;
let newUrl = internalEvent.url;
if (rewriteUrl) {
newUrl = rewriteUrl;
// If not a string, it should probably throw
if (isExternal(newUrl, internalEvent.headers.host as string)) {
isExternalRewrite = true;
} else {
const rewriteUrlObject = new URL(rewriteUrl);
// Search params from the rewritten URL override the original search params

middlewareQuery = getQueryFromSearchParams(rewriteUrlObject.searchParams);

// We still need to add internal search params to the query string for pages router on older versions of Next.js
if ("__nextDataReq" in internalEvent.query) {
middlewareQuery.__nextDataReq = internalEvent.query.__nextDataReq;
}
}
}
}

// If the middleware wants to directly return a response (i.e. not using `NextResponse.next()` or `NextResponse.rewrite()`)
// we return the response directly
if (!rewriteUrl && !responseHeaders.get("x-middleware-next")) {
// transfer response body to res
const body = (result.body as ReadableStream) ?? emptyReadableStream();
// If the middleware wants to directly return a response (i.e. not using `NextResponse.next()` or `NextResponse.rewrite()`)
// we return the response directly
if (!rewriteUrl && !responseHeaders.get("x-middleware-next")) {
// transfer response body to res
const body = (result.body as ReadableStream) ?? emptyReadableStream();

return {
type: internalEvent.type,
statusCode: statusCode,
headers: resHeaders,
body,
isBase64Encoded: false,
} satisfies InternalResult;
}

return {
const middlewareEvent = {
responseHeaders: resHeaders,
url: newUrl,
rawPath: new URL(newUrl).pathname,
type: internalEvent.type,
statusCode: statusCode,
headers: resHeaders,
body,
isBase64Encoded: false,
} satisfies InternalResult;
headers: { ...internalEvent.headers, ...reqHeaders },
body: bodyForForward,
method: internalEvent.method,
query: middlewareQuery,
cookies: internalEvent.cookies,
remoteAddress: internalEvent.remoteAddress,
isExternalRewrite,
rewriteStatusCode: rewriteUrl && !isExternalRewrite ? statusCode : undefined,
} satisfies MiddlewareEvent;
bodyWasForwarded = true;
return middlewareEvent;
} finally {
// Do not await one cancellation: a tee branch settles only when both branches finish.
void bodyForMiddleware?.cancel().catch(() => {});
if (!bodyWasForwarded) {
void bodyForForward?.cancel().catch(() => {});
}
}

return {
responseHeaders: resHeaders,
url: newUrl,
rawPath: new URL(newUrl).pathname,
type: internalEvent.type,
headers: { ...internalEvent.headers, ...reqHeaders },
body: internalEvent.body,
method: internalEvent.method,
query: middlewareQuery,
cookies: internalEvent.cookies,
remoteAddress: internalEvent.remoteAddress,
isExternalRewrite,
rewriteStatusCode: rewriteUrl && !isExternalRewrite ? statusCode : undefined,
} satisfies MiddlewareEvent;
}
9 changes: 2 additions & 7 deletions packages/core/src/core/routing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,10 @@ export function unescapeRegex(str: string) {
/**
* @__PURE__
*/
export function convertBodyToReadableStream(method: string, body?: string | Buffer) {
export function convertBodyToReadableStream(method: string, body?: ReadableStream) {
if (method === "GET" || method === "HEAD") return undefined;
if (!body) return undefined;
return new ReadableStream({
start(controller) {
controller.enqueue(body);
controller.close();
},
});
return body;
}

enum CommonHeaders {
Expand Down
Loading
Loading