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
14 changes: 11 additions & 3 deletions packages/open-next/src/core/routing/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ type MiddlewareOutputEvent = InternalEvent & {
externalRewrite?: boolean;
};

type Middleware = (request: Request) => Response | Promise<Response>;
type MiddlewareLoader = () => Promise<{ default: Middleware }>;

function defaultMiddlewareLoader() {
// @ts-expect-error - This is bundled
return import("./middleware.mjs");
}

// NOTE: As of Nextjs 13.4.13+, the middleware is handled outside the next-server.
// OpenNext will run the middleware in a sandbox and set the appropriate req headers
// and res.body prior to processing the next-server.
Expand All @@ -36,6 +44,7 @@ type MiddlewareOutputEvent = InternalEvent & {
// if res.end() is return, the parent needs to return and not process next server
export async function handleMiddleware(
internalEvent: InternalEvent,
middlewareLoader: MiddlewareLoader = defaultMiddlewareLoader,
): Promise<MiddlewareOutputEvent | InternalResult> {
const { query } = internalEvent;
const normalizedPath = localizePath(internalEvent);
Expand All @@ -53,8 +62,7 @@ export async function handleMiddleware(
const url = initialUrl.toString();
// console.log("url", url, normalizedPath);

// @ts-expect-error - This is bundled
const middleware = await import("./middleware.mjs");
const middleware = await middlewareLoader();

const result: Response = await middleware.default({
geo: {
Expand All @@ -73,7 +81,7 @@ export async function handleMiddleware(
},
url,
body: convertBodyToReadableStream(internalEvent.method, internalEvent.body),
});
} as unknown as Request);
const statusCode = result.status;

/* Apply override headers from middleware
Expand Down
260 changes: 260 additions & 0 deletions packages/tests-unit/tests/core/routing/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { handleMiddleware } from "@opennextjs/aws/core/routing/middleware.js";
import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js";
import { InternalEvent } from "@opennextjs/aws/types/open-next.js";
import { toReadableStream } from "@opennextjs/aws/utils/stream.js";
import { vi } from "vitest";

vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({
NextConfig: {},
MiddlewareManifest: {
sortedMiddleware: ["/"],
middleware: {
"/": {
files: [
"prerender-manifest.js",
"server/edge-runtime-webpack.js",
"server/middleware.js",
],
name: "middleware",
page: "/",
matchers: [
{
regexp:
"^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/((?!_next|favicon.ico|match|static|fonts|api\\/auth|og).*))(.json)?[\\/#\\?]?$",
originalSource:
"/((?!_next|favicon.ico|match|static|fonts|api/auth|og).*)",
},
],
wasm: [],
assets: [],
},
},
functions: {},
version: 2,
},
}));

vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({
localizePath: (event: InternalEvent) => event.rawPath,
}));

const middleware = vi.fn();
const middlewareLoader = vi.fn().mockResolvedValue({
default: middleware,
});

type PartialEvent = Partial<
Omit<InternalEvent, "body" | "rawPath" | "query">
> & { body?: string };

function createEvent(event: PartialEvent): InternalEvent {
const [rawPath, qs] = (event.url ?? "/").split("?", 2);
return {
type: "core",
method: event.method ?? "GET",
rawPath,
url: event.url ?? "/",
body: Buffer.from(event.body ?? ""),
headers: event.headers ?? {},
query: convertFromQueryString(qs ?? ""),
cookies: event.cookies ?? {},
remoteAddress: event.remoteAddress ?? "::1",
};
}

beforeEach(() => {
vi.clearAllMocks();
});

/**
* Ideally these tests would be broken up and tests smaller parts of the middleware rather than the entire function.
*/
describe("handleMiddleware", () => {
it("should bypass middlware for internal requests", async () => {
const event = createEvent({
headers: {
"x-isr": "1",
},
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).not.toBeCalled();
expect(result).toEqual(event);
});

it("should invoke middlware with redirect", async () => {
const event = createEvent({});
middleware.mockResolvedValue({
status: 302,
headers: new Headers({
location: "/redirect",
}),
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result.statusCode).toEqual(302);
expect(result.headers.location).toEqual("/redirect");
});

it("should invoke middlware with external redirect", async () => {
const event = createEvent({});
middleware.mockResolvedValue({
status: 302,
headers: new Headers({
location: "http://external/redirect",
}),
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result.statusCode).toEqual(302);
expect(result.headers.location).toEqual("http://external/redirect");
});

it("should invoke middlware with rewrite", async () => {
const event = createEvent({
headers: {
host: "localhost",
},
});
middleware.mockResolvedValue({
headers: new Headers({
"x-middleware-rewrite": "http://localhost/rewrite",
}),
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result).toEqual({
...event,
rawPath: "/rewrite",
url: "/rewrite",
responseHeaders: {
"x-middleware-rewrite": "http://localhost/rewrite",
},
externalRewrite: false,
});
});

it("should invoke middlware with rewrite with __nextDataReq", async () => {
const event = createEvent({
url: "/rewrite?__nextDataReq=1&key=value",
headers: {
host: "localhost",
},
});
middleware.mockResolvedValue({
headers: new Headers({
"x-middleware-rewrite": "http://localhost/rewrite?newKey=value",
}),
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result).toEqual({
...event,
rawPath: "/rewrite",
url: "/rewrite",
responseHeaders: {
"x-middleware-rewrite": "http://localhost/rewrite?newKey=value",
},
query: {
__nextDataReq: "1",
newKey: "value",
},
externalRewrite: false,
});
});

it("should invoke middlware with external rewrite", async () => {
const event = createEvent({
headers: {
host: "localhost",
},
});
middleware.mockResolvedValue({
headers: new Headers({
"x-middleware-rewrite": "http://external/rewrite",
}),
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result).toEqual({
...event,
rawPath: "http://external/rewrite",
url: "http://external/rewrite",
responseHeaders: {
"x-middleware-rewrite": "http://external/rewrite",
},
externalRewrite: true,
});
});

it("should map x-middleware-request- headers as request headers", async () => {
const event = createEvent({});
middleware.mockResolvedValue({
headers: new Headers({
"x-middleware-request-custom-header": "value",
}),
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result).toEqual({
...event,
headers: {
"custom-header": "value",
},
responseHeaders: {},
externalRewrite: false,
});
});

it("should return a response from middleware", async () => {
const event = createEvent({});
const body = toReadableStream("Hello, world!");

middleware.mockResolvedValue({
status: 200,
headers: new Headers(),
body,
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result).toEqual({
type: "core",
statusCode: 200,
headers: {},
body,
isBase64Encoded: false,
});
});

it("should return a response from middleware with set-cookie header", async () => {
const event = createEvent({});
const body = toReadableStream("Hello, world!");

middleware.mockResolvedValue({
status: 200,
headers: new Headers({
"set-cookie": "cookie=value",
}),
body,
});
const result = await handleMiddleware(event, middlewareLoader);

expect(middlewareLoader).toBeCalled();
expect(result).toEqual({
type: "core",
statusCode: 200,
headers: {
"set-cookie": ["cookie=value"],
},
body,
isBase64Encoded: false,
});
});
});
Loading