Skip to content

Node.js middleware 500s with ESM-only serverExternalPackages (nextHandler is not a function) #3560

Description

@adamhake

Reproduction

https://github.com/adamhake/netlify-node-middleware-esm-repro

npm install
npx netlify build --offline
npx netlify serve --offline --port 8899

curl -i http://localhost:8899/protected   # 500, "nextHandler is not a function"
curl -i http://localhost:8899/            # 200 (not in the matcher)

Control in the same repo: node scripts/set-case.mjs cjs swaps the ESM external (nanoid) for a CJS one (ms). Nothing else changes. /protected then returns 200.

next dev and next start both work. The build succeeds. Failure is only when the Node.js middleware runs as a Netlify edge function.

Reproduces on turbopack (Next 16 default) and webpack.

Expected

Node.js middleware (src/proxy.ts) that imports an ESM-only package listed in serverExternalPackages should run on Netlify the same way it does with next start.

Actual

Matched routes return HTTP 500 nextHandler is not a function.

Unhandled Rejection: Error: Failed to load external module nanoid-782ff4eee3a5ce9a:
  TypeError: Import "nanoid-782ff4eee3a5ce9a" not a dependency and not in import map
  from ".../server/.next/server/chunks/[turbopack]_runtime.js"

[___netlify-edge-handler-node-middleware] TypeError: nextHandler is not a function
    at handleMiddleware (edge-runtime/middleware.ts:63:26)

The edge function still loads after that rejection (Loaded edge function Next.js Middleware Handler); the default export is just not a function.

Environment

  • next 16.3.0
  • @netlify/plugin-nextjs 5.15.13 (latest on npm as of 2026-08-21; also present on main)
  • netlify-cli 26.1.0, Node 22.22.2, Deno edge bootstrap v2-17-1
  • ESM package: nanoid@5.1.16 ("type": "module")
  • CJS control: ms@2.1.3

Root cause

serverExternalPackages makes Next emit a runtime import() of a bare specifier (turbopack hashes it: nanoid-782ff4eee3a5ce9a; webpack uses nanoid). Node.js middleware is wrapped as a Deno edge function whose import map does not include that specifier.

copyHandlerDependenciesForNodeMiddleware does inline the package as virtual CJS modules and registers the hashed specifier as a virtual CJS symlink (nanoid-782ff4eee3a5ce9a../../node_modules/nanoid). That registry is only reachable through the patched require() in edge-runtime/lib/cjs.ts. An ESM external never goes through require().

deno_import_map is not a workaround: with webpack it can clear the import-map error, but the route still 500s; with turbopack the specifier is a build-dependent hash.

Unreleased 5.16.0 (#3554) only awaits the generated require(). It does not add import-map entries and does not change cjs.ts.

Suggested fix

Either add traced ESM externals to the edge function import map, or fail the build when middleware reaches an ESM package in serverExternalPackages — the same treatment .node addons already get in copyHandlerDependenciesForNodeMiddleware.

Leaving the package out of serverExternalPackages lets Next bundle it and avoids the 500 for packages like nanoid with no native code. That is not viable when the package must be external.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions