Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(middleware): issues warnings when using node.js global APIs in middleware #36980

Merged
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
86 changes: 78 additions & 8 deletions packages/next/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getSortedRoutes } from '../../../shared/lib/router/utils'
import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack'
import {
EDGE_RUNTIME_WEBPACK,
EDGE_UNSUPPORTED_NODE_APIS,
MIDDLEWARE_BUILD_MANIFEST,
MIDDLEWARE_FLIGHT_MANIFEST,
MIDDLEWARE_MANIFEST,
Expand Down Expand Up @@ -113,7 +114,7 @@ function getCodeAnalizer(params: {
* but actually execute the expression.
*/
const handleWrapExpression = (expr: any) => {
if (parser.state.module?.layer !== 'middleware') {
if (!isInMiddlewareLayer(parser)) {
return
}

Expand Down Expand Up @@ -141,7 +142,7 @@ function getCodeAnalizer(params: {
* module path that is using it.
*/
const handleExpression = () => {
if (parser.state.module?.layer !== 'middleware') {
if (!isInMiddlewareLayer(parser)) {
return
}

Expand Down Expand Up @@ -175,7 +176,7 @@ function getCodeAnalizer(params: {
}

buildInfo.nextUsedEnvVars.add(members[1])
if (parser.state.module?.layer !== 'middleware') {
if (!isInMiddlewareLayer(parser)) {
return true
}
}
Expand All @@ -187,7 +188,7 @@ function getCodeAnalizer(params: {
const handleNewResponseExpression = (node: any) => {
const firstParameter = node?.arguments?.[0]
if (
isUserMiddlewareUserFile(parser.state.current) &&
isInMiddlewareFile(parser) &&
firstParameter &&
!isNullLiteral(firstParameter) &&
!isUndefinedIdentifier(firstParameter)
Expand All @@ -210,8 +211,7 @@ function getCodeAnalizer(params: {
* A noop handler to skip analyzing some cases.
* Order matters: for it to work, it must be registered first
*/
const skip = () =>
parser.state.module?.layer === 'middleware' ? true : undefined
const skip = () => (isInMiddlewareLayer(parser) ? true : undefined)

for (const prefix of ['', 'global.']) {
hooks.expression.for(`${prefix}Function.prototype`).tap(NAME, skip)
Expand All @@ -226,6 +226,7 @@ function getCodeAnalizer(params: {
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
registerUnsupportedApiHooks(parser, compilation)
}
}

Expand Down Expand Up @@ -454,9 +455,78 @@ function getEntryFiles(entryFiles: string[], meta: EntryMetadata) {
return files
}

function isUserMiddlewareUserFile(module: any) {
function registerUnsupportedApiHooks(
parser: webpack5.javascript.JavascriptParser,
compilation: webpack5.Compilation
) {
const { WebpackError } = compilation.compiler.webpack
for (const expression of EDGE_UNSUPPORTED_NODE_APIS) {
const warnForUnsupportedApi = (node: any) => {
if (!isInMiddlewareLayer(parser)) {
return
}
compilation.warnings.push(
makeUnsupportedApiError(WebpackError, parser, expression, node.loc)
)
return true
}
parser.hooks.call.for(expression).tap(NAME, warnForUnsupportedApi)
parser.hooks.expression.for(expression).tap(NAME, warnForUnsupportedApi)
parser.hooks.callMemberChain
.for(expression)
.tap(NAME, warnForUnsupportedApi)
parser.hooks.expressionMemberChain
.for(expression)
.tap(NAME, warnForUnsupportedApi)
}

const warnForUnsupportedProcessApi = (node: any, [callee]: string[]) => {
if (!isInMiddlewareLayer(parser) || callee === 'env') {
return
}
compilation.warnings.push(
makeUnsupportedApiError(
WebpackError,
parser,
`process.${callee}`,
node.loc
)
)
return true
}

parser.hooks.callMemberChain
.for('process')
.tap(NAME, warnForUnsupportedProcessApi)
parser.hooks.expressionMemberChain
.for('process')
.tap(NAME, warnForUnsupportedProcessApi)
}

function makeUnsupportedApiError(
WebpackError: typeof webpack5.WebpackError,
parser: webpack5.javascript.JavascriptParser,
name: string,
loc: any
) {
const error = new WebpackError(
`You're using a Node.js API (${name} at line: ${loc.start.line}) which is not supported in the Edge Runtime that Middleware uses.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`
)
error.name = NAME
error.module = parser.state.current
error.loc = loc
return error
}

function isInMiddlewareLayer(parser: webpack5.javascript.JavascriptParser) {
return parser.state.module?.layer === 'middleware'
}

function isInMiddlewareFile(parser: webpack5.javascript.JavascriptParser) {
return (
module.layer === 'middleware' && /middleware\.\w+$/.test(module.rawRequest)
parser.state.current?.layer === 'middleware' &&
/middleware\.\w+$/.test(parser.state.current?.rawRequest)
)
}

Expand Down
106 changes: 85 additions & 21 deletions packages/next/server/web/sandbox/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'next/dist/compiled/abort-controller'
import vm from 'vm'
import type { WasmBinding } from '../../../build/webpack/loaders/get-module-build-info'
import { EDGE_UNSUPPORTED_NODE_APIS } from '../../../shared/lib/constants'

const WEBPACK_HASH_REGEX =
/__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g
Expand Down Expand Up @@ -47,19 +48,21 @@ const caches = new Map<
}
>()

interface ModuleContextOptions {
module: string
onWarning: (warn: Error) => void
useCache: boolean
env: string[]
wasm: WasmBinding[]
}

/**
* For a given module name this function will create a context for the
* runtime. It returns a function where we can provide a module path and
* run in within the context. It may or may not use a cache depending on
* the parameters.
*/
export async function getModuleContext(options: {
module: string
onWarning: (warn: Error) => void
useCache: boolean
env: string[]
wasm: WasmBinding[]
}) {
export async function getModuleContext(options: ModuleContextOptions) {
let moduleCache = options.useCache
? caches.get(options.module)
: await createModuleContext(options)
Expand Down Expand Up @@ -97,12 +100,7 @@ export async function getModuleContext(options: {
* 2. Dependencies that require runtime globals such as Blob.
* 3. Dependencies that are scoped for the provided parameters.
*/
async function createModuleContext(options: {
onWarning: (warn: Error) => void
module: string
env: string[]
wasm: WasmBinding[]
}) {
async function createModuleContext(options: ModuleContextOptions) {
const requireCache = new Map([
[require.resolve('next/dist/compiled/cookie'), { exports: cookie }],
])
Expand Down Expand Up @@ -181,11 +179,10 @@ async function createModuleContext(options: {
* Create a base context with all required globals for the runtime that
* won't depend on any externally provided dependency.
*/
function createContext(options: {
/** Environment variables to be provided to the context */
env: string[]
}) {
const context: { [key: string]: unknown } = {
function createContext(
options: Pick<ModuleContextOptions, 'env' | 'onWarning'>
) {
const context: Context = {
_ENTRIES: {},
atob: polyfills.atob,
Blob,
Expand All @@ -207,19 +204,20 @@ function createContext(options: {
CryptoKey: polyfills.CryptoKey,
Crypto: polyfills.Crypto,
crypto: new polyfills.Crypto(),
DataView,
File,
FormData,
process: {
env: buildEnvironmentVariablesFrom(options.env),
},
process: createProcessPolyfill(options),
ReadableStream,
setInterval,
setTimeout,
queueMicrotask,
TextDecoder,
TextEncoder,
TransformStream,
URL,
URLSearchParams,
WebAssembly,

// Indexed collections
Array,
Expand All @@ -244,6 +242,18 @@ function createContext(options: {
// Structured data
ArrayBuffer,
SharedArrayBuffer,

// These APIs are supported by the Edge runtime, but not by the version of Node.js we're using
// Since we'll soon replace this sandbox with the edge-runtime itself, it's not worth polyfilling.
// ReadableStreamBYOBReader,
// ReadableStreamDefaultReader,
// structuredClone,
// SubtleCrypto,
// WritableStream,
// WritableStreamDefaultWriter,
}
for (const name of EDGE_UNSUPPORTED_NODE_APIS) {
addStub(context, name, options)
}

// Self references
Expand Down Expand Up @@ -286,3 +296,57 @@ async function loadWasm(

return modules
}

function createProcessPolyfill(
options: Pick<ModuleContextOptions, 'env' | 'onWarning'>
) {
const env = buildEnvironmentVariablesFrom(options.env)

const processPolyfill = { env }
const overridenValue: Record<string, any> = {}
for (const key of Object.keys(process)) {
if (key === 'env') continue
Object.defineProperty(processPolyfill, key, {
get() {
emitWarning(`process.${key}`, options)
return overridenValue[key]
},
set(value) {
overridenValue[key] = value
},
enumerable: false,
})
}
return processPolyfill
}

const warnedAlready = new Set<string>()

function addStub(
context: Context,
name: string,
contextOptions: Pick<ModuleContextOptions, 'onWarning'>
) {
Object.defineProperty(context, name, {
get() {
emitWarning(name, contextOptions)
return undefined
},
enumerable: false,
})
}

function emitWarning(
name: string,
contextOptions: Pick<ModuleContextOptions, 'onWarning'>
) {
if (!warnedAlready.has(name)) {
const warning =
new Error(`You're using a Node.js API (${name}) which is not supported in the Edge Runtime that Middleware uses.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
warning.name = 'NodejsRuntimeApiInMiddlewareWarning'
contextOptions.onWarning(warning)
console.warn(warning.message)
warnedAlready.add(name)
}
}
28 changes: 28 additions & 0 deletions packages/next/shared/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,31 @@ export const OPTIMIZED_FONT_PROVIDERS = [
]
export const STATIC_STATUS_PAGES = ['/500']
export const TRACE_OUTPUT_VERSION = 1

// comparing
// https://nextjs.org/docs/api-reference/edge-runtime
// with
// https://nodejs.org/docs/latest/api/globals.html
export const EDGE_UNSUPPORTED_NODE_APIS = [
'clearImmediate',
'setImmediate',
'BroadcastChannel',
'Buffer',
'ByteLengthQueuingStrategy',
'CompressionStream',
'CountQueuingStrategy',
'DecompressionStream',
'DomException',
'Event',
'EventTarget',
'MessageChannel',
'MessageEvent',
'MessagePort',
'ReadableByteStreamController',
'ReadableStreamBYOBRequest',
'ReadableStreamDefaultController',
'TextDecoderStream',
'TextEncoderStream',
'TransformStreamDefaultController',
'WritableStreamDefaultController',
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function middleware() {
process.cwd = () => 'fixed-value'
console.log(process.cwd(), process.env)
return new Response()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <div>A page</div>
}
Loading