Skip to content

Commit

Permalink
fix(edge-runtime): undefined global in edge runtime. (#38769)
Browse files Browse the repository at this point in the history
## How to reproduce

1. create a next.js app with a middleware (or an edge route) that imports a node.js module:
   ```js
   // middleware.js
   import { NextResponse } from 'next/server'
   import { basename } from 'path'
   
   export default async function middleware() {
     basename()
     return NextResponse.next()
   }
   ```
2. deploy it to vercel with `vc`
3. go to the your function logs in Vercel Front (https://vercel.com/$user/$project/$deployment/functions)
4. in another tab, query your application
   > it results in a 500 page:
   <img width="517" alt="image" src="https://user-images.githubusercontent.com/186268/179557102-72568ca9-bcfd-49e2-9b9c-c51c3064f2d7.png">

    >  in the logs you should see:
   <img width="1220" alt="image" src="https://user-images.githubusercontent.com/186268/179557266-498f3290-b7df-46ac-8816-7bb396821245.png">

## Expected behavior

The route should fail indeed in a 500, because Edge runtime **does not support node.js modules**. However the error in logs should be completely different:
```shell
error - Error: The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
```

## Notes to reviewers

I introduced this issue in #38234.
Prior to the PR above, the app would not even build, as we were checking imported node.js module during the build with AST analysis.
Since #38234, the app would build and should fail at runtime, with an appropriate error.

The mistake was to declare `__import_unsupported` function in the sandbox's context, that is only used in `next dev` and `next start`, but not shipped to Vercel platform.

By loading it inside webpack loaders (both middleware and edge route), we ensure it will be defined on Vercel as well. 

The existing test suite (`pnpm testheadless --testPathPattern=runtime-module-error`) covers them.
  • Loading branch information
feugy committed Jul 20, 2022
1 parent 32a346b commit 1dcba39
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,9 @@ export default function middlewareLoader(this: any) {
}

return `
import { adapter } from 'next/dist/server/web/adapter'
import { adapter, enhanceGlobals } from 'next/dist/server/web/adapter'
// The condition is true when the "process" module is provided
if (process !== global.process) {
// prefer local process but global.process has correct "env"
process.env = global.process.env;
global.process = process;
}
enhanceGlobals()
var mod = require(${stringifiedPagePath})
var handler = mod.middleware || mod.default;
Expand Down
9 changes: 2 additions & 7 deletions packages/next/build/webpack/loaders/next-middleware-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,9 @@ export default function middlewareLoader(this: any) {
}

return `
import { adapter, blockUnallowedResponse } from 'next/dist/server/web/adapter'
import { adapter, blockUnallowedResponse, enhanceGlobals } from 'next/dist/server/web/adapter'
// The condition is true when the "process" module is provided
if (process !== global.process) {
// prefer local process but global.process has correct "env"
process.env = global.process.env;
global.process = process;
}
enhanceGlobals()
var mod = require(${stringifiedPagePath})
var handler = mod.middleware || mod.default;
Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export async function handleWebpackExtenalForEdgeRuntime({
contextInfo: any
}) {
if (contextInfo.issuerLayer === 'middleware' && isNodeJsModule(request)) {
return `root __import_unsupported('${request}')`
return `root globalThis.__import_unsupported('${request}')`
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ export default class DevServer extends Server {
const frames = parseStack(err.stack!)
const frame = frames.find(
({ file }) =>
!file?.startsWith('eval') && !file?.includes('sandbox/context')
!file?.startsWith('eval') && !file?.includes('web/adapter')
)!

if (frame.lineNumber && frame?.file) {
Expand Down
44 changes: 44 additions & 0 deletions packages/next/server/web/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,50 @@ export function blockUnallowedResponse(
})
}

export function enhanceGlobals() {
// The condition is true when the "process" module is provided
if (process !== global.process) {
// prefer local process but global.process has correct "env"
process.env = global.process.env
global.process = process
}

// to allow building code that import but does not use node.js modules,
// webpack will expect this function to exist in global scope
Object.defineProperty(globalThis, '__import_unsupported', {
value: __import_unsupported,
enumerable: false,
configurable: false,
})
}

function __import_unsupported(moduleName: string) {
const proxy: any = new Proxy(function () {}, {
get(_obj, prop) {
if (prop === 'then') {
return {}
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
construct() {
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
apply(_target, _this, args) {
if (typeof args[0] === 'function') {
return args[0](proxy)
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
})
return new Proxy({}, { get: () => proxy })
}

function getUnsupportedModuleErrorMessage(module: string) {
// warning: if you change these messages, you must adjust how react-dev-overlay's middleware detects modules not found
return `The edge runtime does not support Node.js '${module}' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`
}

class NextRequestHint extends NextRequest {
sourcePage: string

Expand Down
29 changes: 0 additions & 29 deletions packages/next/server/web/sandbox/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,29 +132,6 @@ async function createModuleContext(options: ModuleContextOptions) {
return fn()
}

context.__import_unsupported = function __import_unsupported(
moduleName: string
) {
const proxy: any = new Proxy(function () {}, {
get(_obj, prop) {
if (prop === 'then') {
return {}
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
construct() {
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
apply(_target, _this, args) {
if (args[0] instanceof Function) {
return args[0](proxy)
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
})
return new Proxy({}, { get: () => proxy })
}

context.__next_webassembly_compile__ =
function __next_webassembly_compile__(fn: Function) {
const key = fn.toString()
Expand Down Expand Up @@ -358,9 +335,3 @@ function decorateUnhandledError(error: any) {
decorateServerError(error, 'edge-server')
}
}

function getUnsupportedModuleErrorMessage(module: string) {
// warning: if you change these messages, you must adjust how react-dev-overlay's middleware detects modules not found
return `The edge runtime does not support Node.js '${module}' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`
}

0 comments on commit 1dcba39

Please sign in to comment.