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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

export function middleware(request: NextRequest) {
export default function middleware(request: NextRequest) {
const path = request.nextUrl.pathname; //new URL(request.url).pathname;

const host = request.headers.get("host");
Expand Down
44 changes: 43 additions & 1 deletion packages/cloudflare/src/cli/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import { compileImages } from "./build/open-next/compile-images.js";
import { compileInit } from "./build/open-next/compile-init.js";
import { compileSkewProtection } from "./build/open-next/compile-skew-protection.js";
import { compileDurableObjects } from "./build/open-next/compileDurableObjects.js";
import { patchWebpackMiddlewareRuntime } from "./build/patches/ast/webpack-runtime.js";
import { inlineLoadManifest } from "./build/patches/plugins/load-manifest.js";
import { patchOpenTelemetryGlobalUtils } from "./build/patches/plugins/opentelemetry.js";
import { patchResRevalidate } from "./build/patches/plugins/res-revalidate.js";
import { patchTurbopackRuntime } from "./build/patches/plugins/turbopack.js";
import { patchUseCacheIO } from "./build/patches/plugins/use-cache.js";
Expand Down Expand Up @@ -68,7 +70,47 @@ export default buildAdapter((config: OpenNextConfig, buildOpts: BuildOptions) =>
isInCloudflare: true,
}),
],
additionalCodePatches: [patchResRevalidate, patchUseCacheIO, patchTurbopackRuntime],
additionalCodePatches: [
patchResRevalidate,
patchUseCacheIO,
patchOpenTelemetryGlobalUtils,
patchTurbopackRuntime,
],
},
middlewareBundle: {
useEdgeConfig: true,
banner: (_name: string) => [
`globalThis.monorepoPackagePath = "${normalizePath(packagePath)}";`,
`import { Buffer } from "node:buffer";
globalThis.Buffer = Buffer;

import { AsyncLocalStorage } from "node:async_hooks";
globalThis.AsyncLocalStorage = AsyncLocalStorage;

`,
],
additionalPlugins: (updater: ContentUpdater, outputs: NextAdapterOutputs) => [
inlineRouteHandler(updater, outputs, packagePath),
inlineLoadManifest(updater, buildOpts),
...(config.middleware?.external
? [
openNextExternalMiddlewarePlugin(
path.join(buildOpts.openNextDistDir, "core/edgeFunctionHandler.js")
),
]
Comment on lines +95 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Middleware handler plugin added twice with conflicting target paths in Cloudflare builds

The external-middleware plugin is registered twice with different handler paths (openNextExternalMiddlewarePlugin(...) at packages/core/src/build/middleware/buildNodeMiddleware.ts:137-139 and again via additionalPlugins at packages/cloudflare/src/cli/adapter.ts:95-100), so the first plugin wins and the adapter's intended handler is silently ignored.

Impact: On Cloudflare, the middleware bundle may use the wrong handler module, potentially breaking middleware execution at runtime.

Duplicate esbuild plugin with conflicting resolve targets

The core function buildExternalNodeMiddleware unconditionally adds openNextExternalMiddlewarePlugin pointing to core/nodeMiddlewareHandler.js at packages/core/src/build/middleware/buildNodeMiddleware.ts:137-139. Then it spreads ...additionalPlugins at line 140, which for the Cloudflare adapter includes another openNextExternalMiddlewarePlugin pointing to core/edgeFunctionHandler.js (see packages/cloudflare/src/cli/adapter.ts:95-100).

Since buildExternalNodeMiddleware is only called when config.middleware?.external is true, the conditional guard in the Cloudflare adapter's middlewareBundle.additionalPlugins (config.middleware?.external ? [...] : []) always evaluates to true in this code path, meaning the plugin is always duplicated.

In esbuild, plugins are processed in registration order, so the core's plugin (registered first) resolves the import to nodeMiddlewareHandler.js, and the adapter's plugin (registered second) never gets a chance to redirect it to edgeFunctionHandler.js.

Prompt for agents
The Cloudflare adapter's middlewareBundle.additionalPlugins includes openNextExternalMiddlewarePlugin with edgeFunctionHandler.js, but the core's buildExternalNodeMiddleware (packages/core/src/build/middleware/buildNodeMiddleware.ts:137-139) already unconditionally adds the same plugin type with nodeMiddlewareHandler.js. This results in a duplicate plugin where the core's version takes precedence.

Two possible fixes:
1. Remove the openNextExternalMiddlewarePlugin from the Cloudflare adapter's middlewareBundle.additionalPlugins, since the core already adds it. But this only works if nodeMiddlewareHandler.js is the correct handler for Cloudflare middleware.
2. If the Cloudflare adapter needs edgeFunctionHandler.js instead, then the core's buildExternalNodeMiddleware should NOT hardcode the plugin, and instead let the adapter provide it via additionalPlugins. This would require removing lines 137-139 from buildNodeMiddleware.ts and ensuring all adapters that use external node middleware include the plugin in their middlewareBundle.additionalPlugins.

The right approach depends on whether Cloudflare middleware should use nodeMiddlewareHandler.js or edgeFunctionHandler.js.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

: []),
openNextEdgePlugins({
nextDir: path.join(buildOpts.appBuildOutputPath, ".next"),
isInCloudflare: true,
}),
],
additionalCodePatches: [
patchResRevalidate,
patchUseCacheIO,
patchOpenTelemetryGlobalUtils,
patchWebpackMiddlewareRuntime,
patchTurbopackRuntime,
],
},
afterServerBundle: async (buildOpts, _config) => {
compileDurableObjects(buildOpts);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js";
import { describe, expect, test } from "vitest";

import { buildMultipleChunksRule, singleChunkRule } from "./webpack-runtime.js";
import {
buildMultipleChunksRule,
patchWebpackMiddlewareRuntime,
singleChunkRule,
} from "./webpack-runtime.js";

describe("webpack runtime", () => {
describe("multiple chunks", () => {
Expand Down Expand Up @@ -110,4 +114,33 @@ describe("webpack runtime", () => {
`);
});
});

describe("middleware runtime", () => {
const runtimeCode = `
t.f.require=(o,n)=>{e[o]||(658!=o?r(require("./chunks/"+t.u(o))):e[o]=1)}
`;

test("uses the middleware traced chunks", async () => {
const patch = patchWebpackMiddlewareRuntime.patches[0]!;
const result = await patch.patchCode({
code: runtimeCode,
tracedFiles: ["/app/.open-next/middleware/app/.next/server/chunks/123.js"],
} as never);

expect(result).toContain('case 123: r(require("./chunks/123.js")); break;');
expect(result).not.toContain('require("./chunks/" +');
});

test("matches minified runtime code", () => {
expect(patchWebpackMiddlewareRuntime.patches[0]!.contentFilter?.test(runtimeCode)).toBe(true);
});

test("supports middleware with no chunks", async () => {
const patch = patchWebpackMiddlewareRuntime.patches[0]!;
const result = await patch.patchCode({ code: runtimeCode, tracedFiles: [] } as never);

expect(result).toContain("case 658: e[o] = 1; break;");
expect(result).not.toContain('require("./chunks/" +');
});
});
});
54 changes: 47 additions & 7 deletions packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { join } from "node:path";

import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js";
import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js";
import type { CodePatcher } from "@opennextjs/core/build/patch/codePatcher.js";
import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js";

// Inline the code when there are multiple chunks
export function buildMultipleChunksRule(chunks: number[]) {
Expand Down Expand Up @@ -69,6 +71,44 @@ fix: |
}
`;

export function patchWebpackRuntimeCode(code: string, chunks: number[]): string {
let patched = patchCode(code, buildMultipleChunksRule(chunks));
patched = patchCode(patched, singleChunkRule);
return patched;
}

function getWebpackChunks(tracedFiles: string[]): number[] {
const chunks = new Set<number>();
for (const file of tracedFiles) {
const match = file.match(/[\\/]chunks[\\/](\d+)\.js$/);
if (match) {
chunks.add(Number(match[1]));
}
}
return Array.from(chunks);
}

/**
* Rewrites webpack runtime chunk loading in an external middleware bundle.
*
* The middleware trace already contains every required chunk. Using the traced
* paths keeps the generated requires visible to the Worker bundler and also
* supports middleware bundles with no chunks.
*/
export const patchWebpackMiddlewareRuntime: CodePatcher = {
name: "inline-webpack-chunks",
patches: [
{
pathFilter: getCrossPlatformPathRegex(String.raw`webpack(?:-api)?-runtime\.js$`, {
escape: false,
}),
contentFilter: /require\("\.\/chunks\/"\s*\+/,
patchCode: async ({ code, tracedFiles }) =>
patchWebpackRuntimeCode(code, getWebpackChunks(tracedFiles)),
},
],
};

/**
* Fixes the webpack-runtime.js and webpack-api-runtime.js files by inlining
* the webpack dynamic requires.
Expand All @@ -84,11 +124,12 @@ export async function patchWebpackRuntime(buildOpts: BuildOptions) {
);

// Look for all the chunks.
const chunks = readdirSync(join(dotNextServerDir, "chunks"))
.filter((chunk) => /^\d+\.js$/.test(chunk))
.map((chunk) => {
return Number(chunk.replace(/\.js$/, ""));
});
const chunksDir = join(dotNextServerDir, "chunks");
const chunks = existsSync(chunksDir)
? readdirSync(chunksDir)
.filter((chunk) => /^\d+\.js$/.test(chunk))
.map((chunk) => Number(chunk.replace(/\.js$/, "")))
: [];

patchFile(join(dotNextServerDir, "webpack-runtime.js"), chunks);
patchFile(join(dotNextServerDir, "webpack-api-runtime.js"), chunks);
Expand All @@ -103,8 +144,7 @@ export async function patchWebpackRuntime(buildOpts: BuildOptions) {
function patchFile(filename: string, chunks: number[]) {
if (existsSync(filename)) {
let code = readFileSync(filename, "utf-8");
code = patchCode(code, buildMultipleChunksRule(chunks));
code = patchCode(code, singleChunkRule);
code = patchWebpackRuntimeCode(code, chunks);
writeFileSync(filename, code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from "vitest";

import { patchOpenTelemetryGlobalUtilsCode } from "./opentelemetry.js";

const code = `"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.unregisterGlobal = exports.getGlobal = exports.registerGlobal = void 0;
const platform_1 = require("../platform");
const version_1 = require("../version");
const semver_1 = require("./semver");
const major = version_1.VERSION.split('.')[0];
const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for(\`opentelemetry.js.api.\${major}\`);
const _global = platform_1._globalThis;
function registerGlobal(type, instance, diag, allowOverride = false) {
const api = (_global[GLOBAL_OPENTELEMETRY_API_KEY] = {
version: version_1.VERSION,
});
return semver_1.isCompatible(api.version);
}
`;

test("patches OpenTelemetry to use the Cloudflare global", () => {
const patched = patchOpenTelemetryGlobalUtilsCode(code);

expect(patched).toContain('// const platform_1 = require("../platform");');
expect(patched).toContain("const _global = globalThis;");
expect(patched).toContain('const version_1 = require("../version");');
expect(patched).toContain("return semver_1.isCompatible(api.version);");
expect(patched).not.toContain("platform_1._globalThis");
});

test("captures the platform and global variable names", () => {
const patched = patchOpenTelemetryGlobalUtilsCode(`
const nodePlatform = require("../platform");
const globalStore = nodePlatform._globalThis;
const unrelatedStore = otherPlatform._globalThis;
`);

expect(patched).toContain('// const nodePlatform = require("../platform");');
expect(patched).toContain("const globalStore = globalThis;");
expect(patched).toContain("const unrelatedStore = otherPlatform._globalThis;");
});
41 changes: 41 additions & 0 deletions packages/cloudflare/src/cli/build/patches/plugins/opentelemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js";
import type { CodePatcher } from "@opennextjs/core/build/patch/codePatcher.js";
import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js";

export const commentPlatformImportRule = `
rule:
pattern: const $PLATFORM = require("../platform");
fix: // const $PLATFORM = require("../platform");
`;

export const replaceGlobalRule = `
rule:
all:
- pattern: const $GLOBAL = $PLATFORM._globalThis;
- inside:
kind: program
stopBy: end
has:
pattern: const $PLATFORM = require("../platform");
fix: const $GLOBAL = globalThis;
`;

export function patchOpenTelemetryGlobalUtilsCode(code: string): string {
let patchedCode = patchCode(code, replaceGlobalRule);
patchedCode = patchCode(patchedCode, commentPlatformImportRule);
return patchedCode;
}

export const patchOpenTelemetryGlobalUtils: CodePatcher = {
name: "patch-opentelemetry-global-utils",
patches: [
{
pathFilter: getCrossPlatformPathRegex(
String.raw`@opentelemetry/api/build/src/internal/global-utils\.js$`,
{ escape: false }
),
contentFilter: /require\(["']\.\.\/platform["']\)/,
patchCode: async ({ code }) => patchOpenTelemetryGlobalUtilsCode(code),
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, test } from "vitest";

import { patchTurbopackRuntime } from "./turbopack.js";

describe("turbopack runtime", () => {
const runtimeCode = `
function loadRuntimeChunkPath(chunkPath) {
const resolved = chunkPath;
return require(resolved);
}
`;

test("uses the middleware traced chunks", async () => {
const patch = patchTurbopackRuntime.patches[0]!;
const result = await patch.patchCode({
code: runtimeCode,
tracedFiles: ["/app/.open-next/middleware/app/.next/server/chunks/ssr/chunk.js"],
} as never);

expect(result).toContain('case "server/chunks/ssr/chunk.js"');
expect(result).toContain(
'return require("/app/.open-next/middleware/app/.next/server/chunks/ssr/chunk.js")'
);
});

test("supports middleware with no chunks", async () => {
const patch = patchTurbopackRuntime.patches[0]!;
const result = await patch.patchCode({ code: runtimeCode, tracedFiles: [] } as never);

expect(result).toContain("function requireChunk(chunkPath)");
expect(result).toContain("default:");
});
});
45 changes: 44 additions & 1 deletion packages/core/src/build/adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,14 @@ describe("buildAdapter", () => {

expect(createMiddleware).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ forceOnlyBuildOnce: true })
expect.objectContaining({ forceOnlyBuildOnce: true }),
expect.objectContaining({
pages: expect.any(Array),
appPages: expect.any(Array),
appRoutes: expect.any(Array),
pagesApi: expect.any(Array),
}),
undefined
);
});

Expand Down Expand Up @@ -426,6 +433,42 @@ describe("buildAdapter", () => {
);
});

test("onBuildComplete forwards middlewareBundle to createMiddleware intact", async () => {
const mockPlugin = { name: "mock-plugin", setup: vi.fn() };
const additionalPlugins = vi.fn(() => [mockPlugin]);
const additionalCodePatches = [{ name: "test-middleware-patch", patches: [] }];
const middlewareBundle = {
additionalPlugins,
additionalCodePatches,
useEdgeConfig: true,
externals: ["./externals-test"],
banner: ["// test banner"],
};

const adapter = buildAdapter(() => ({
middlewareBundle,
}));

const nextConfig = { experimental: {}, images: {} } as BuildCompleteContext["config"];
await adapter.modifyConfig(nextConfig, { phase: "production" });

const ctx = createMockContext();
await adapter.onBuildComplete(ctx);

expect(createMiddleware).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Object),
ctx.outputs,
middlewareBundle
);

const calls = vi.mocked(createMiddleware).mock.calls;
expect(calls).toHaveLength(1);
expect(calls[0]).toHaveLength(4);
expect(calls[0][3]).toEqual(middlewareBundle);
expect(calls[0][3]).toBe(middlewareBundle);
});

test("onBuildComplete compiles tag cache provider when useTagCache is true", async () => {
vi.mocked(createCacheAssets).mockReturnValue({
useTagCache: true,
Expand Down
Loading
Loading