Add support for node middleware in cloudflare - #38
Conversation
commit: |
| ...(config.middleware?.external | ||
| ? [ | ||
| openNextExternalMiddlewarePlugin( | ||
| path.join(buildOpts.openNextDistDir, "core/edgeFunctionHandler.js") | ||
| ), | ||
| ] |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
d1b99ee to
f2de8e2
Compare
7093c5d to
e34ef09
Compare
Independent validation (build + local runtime)I hit this via Next 16 Baseline (released
|
| Route | Result |
|---|---|
HEAD / |
200, middleware cookies (from=middleware, with=love) + response-header: response-header |
HEAD /redirect |
307 → /redirect-destination, Set-Cookie: test=success |
HEAD /cookies |
200, Set-Cookie: foo=bar |
GET /api/middleware |
200 {"hello":"middleware"} |
HEAD /rewrite |
500 SyntaxError: Unexpected end of JSON input (may be HEAD-specific; flagging) |
So the core Node/proxy.ts path works end-to-end for cookies / redirect / direct middleware response. This unblocks Next 16 apps that can no longer opt proxy.ts back to Edge.
Merge blockers I can see
- PR is stacked on
conico/share-build(depends on Make a default overridable adapter in core #35), notmain. - GitHub currently reports
mergeable_state: dirtyvsconico/share-build— needs rebase. - Preview install story is broken for external testers (
@opennextjs/corenot published; shortpkg.pr.newURL points at the wrong repo). Publishing paired@opennextjs/cloudflare+@opennextjs/corepreviews (or documenting monorepo-only testing) would get more validation.
Happy to re-test after rebase, or run a fuller Playwright pass if useful. Real production apps (including mine) are still stuck on the middleware.ts deprecation workaround until this lands.
…y into core adapter feat: enhance build process with additional server bundle customization options chore: update package.json scripts for improved build and testing workflow test: add unit tests for adapter build process and server bundle generation fix: ensure proper handling of external dependencies and edge configuration in server bundle
…ests for resolve plugin
…specific overrides
…udflare specific overrides" This reverts commit 37c4d90.
…ad of throwing - Extract ValidateConfigResult type with success/message/shouldThrow/level - Convert validateFunctionOptions and validateSplittedFunctionOptions to return result objects - Remove logger dependency from validateConfig.ts - Preserve compatibilityMatrix, TODO comment, @ts-expect-error pragmas - Add 5 characterization tests in validateConfig.spec.ts - No caller impact: compileConfig.ts is the sole importer (updated in T3)
- Export OpenNextOutput interface (was internal) - Extract buildOpenNextOutput(buildOpts) for construction-only (no fs write) - Keep legacy generateOutput as thin wrapper (construction + file write) - Preserve all construction logic verbatim, including @ts-expect-error - Add 3 characterization tests in generateOutput.spec.ts - Backward compatible: byte-equivalent output to today
- Replace bare validateConfig(config) call with result-handling block - Throw on shouldThrow:true (bad routes — preserves existing behavior) - Log at appropriate level on shouldThrow:false (level field from T1) - All 3 export signatures and edge-runtime detection block unchanged - Direct callers (aws/build.ts, cloudflare/utils.ts) unaffected
…OpenNextAdapterOptions - Make OpenNextAdapterOptions<T = OpenNextOutput> and buildAdapter<T> generic - Add validateConfig override hook (runs after callback in modifyConfig) - Add generateOutput override hook (returns T, gated by skipGenerateOutput) - buildAdapter serializes override return via fs.writeFileSync (override never touches fs) - Default path uses buildOpenNextOutput (extracted in T2) - Add 5 new tests covering override behaviors + default path + skipGenerateOutput - All 16 existing adapter tests preserved; AWS/Cloudflare adapters compile with default T
When an adapter config specifies full package-specifier paths (e.g., @opennextjs/aws/overrides/wrappers/aws-lambda.js), esbuild cannot resolve them during bundling. Use createRequire(args.path).resolve() in the openNextResolvePlugin to convert package specifiers to filesystem-relative paths at build time, falling back to the original value if resolution fails. This fixes the openbuild:local build error: ERROR: Could not resolve "@opennextjs/aws/overrides/wrappers/aws-lambda.js" ERROR: Could not resolve "@opennextjs/aws/overrides/tagCache/dynamodb.js" Added test I verifying resolution of a mock package in node_modules.
…solution and improve path handling
4e9a367 to
cd8f83e
Compare
e34ef09 to
65cdda7
Compare
…dflare Next.js 16 forces proxy.ts onto the Node.js middleware runtime with no opt-out, and @opennextjs/cloudflare (1.20.2) doesn't support that yet — verified locally, `opennextjs-cloudflare build` fails on a real proxy.ts. middleware.ts + runtime: 'experimental-edge' builds clean on both next build and opennextjs-cloudflare build, same routing logic unchanged. Revert to proxy.ts once opennextjs/adapters-api#38 or opennextjs/opennextjs-cloudflare#1320 (or their successor) ships on npm — see docs/CLOUDFLARE-MIGRATION.md for the full investigation and why those specific PR numbers, not "whenever OpenNext adds support" in general.
Add support for node middleware in cloudflare