Skip to content

Commit 4e6b6a5

Browse files
authored
feat(middleware): issues warnings when using node.js global APIs in middleware (#36980)
1 parent 6f2a8d3 commit 4e6b6a5

File tree

9 files changed

+440
-29
lines changed

9 files changed

+440
-29
lines changed

packages/next/build/webpack/plugins/middleware-plugin.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getSortedRoutes } from '../../../shared/lib/router/utils'
66
import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack'
77
import {
88
EDGE_RUNTIME_WEBPACK,
9+
EDGE_UNSUPPORTED_NODE_APIS,
910
MIDDLEWARE_BUILD_MANIFEST,
1011
MIDDLEWARE_FLIGHT_MANIFEST,
1112
MIDDLEWARE_MANIFEST,
@@ -113,7 +114,7 @@ function getCodeAnalizer(params: {
113114
* but actually execute the expression.
114115
*/
115116
const handleWrapExpression = (expr: any) => {
116-
if (parser.state.module?.layer !== 'middleware') {
117+
if (!isInMiddlewareLayer(parser)) {
117118
return
118119
}
119120

@@ -141,7 +142,7 @@ function getCodeAnalizer(params: {
141142
* module path that is using it.
142143
*/
143144
const handleExpression = () => {
144-
if (parser.state.module?.layer !== 'middleware') {
145+
if (!isInMiddlewareLayer(parser)) {
145146
return
146147
}
147148

@@ -175,7 +176,7 @@ function getCodeAnalizer(params: {
175176
}
176177

177178
buildInfo.nextUsedEnvVars.add(members[1])
178-
if (parser.state.module?.layer !== 'middleware') {
179+
if (!isInMiddlewareLayer(parser)) {
179180
return true
180181
}
181182
}
@@ -187,7 +188,7 @@ function getCodeAnalizer(params: {
187188
const handleNewResponseExpression = (node: any) => {
188189
const firstParameter = node?.arguments?.[0]
189190
if (
190-
isUserMiddlewareUserFile(parser.state.current) &&
191+
isInMiddlewareFile(parser) &&
191192
firstParameter &&
192193
!isNullLiteral(firstParameter) &&
193194
!isUndefinedIdentifier(firstParameter)
@@ -210,8 +211,7 @@ function getCodeAnalizer(params: {
210211
* A noop handler to skip analyzing some cases.
211212
* Order matters: for it to work, it must be registered first
212213
*/
213-
const skip = () =>
214-
parser.state.module?.layer === 'middleware' ? true : undefined
214+
const skip = () => (isInMiddlewareLayer(parser) ? true : undefined)
215215

216216
for (const prefix of ['', 'global.']) {
217217
hooks.expression.for(`${prefix}Function.prototype`).tap(NAME, skip)
@@ -226,6 +226,7 @@ function getCodeAnalizer(params: {
226226
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
227227
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
228228
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
229+
registerUnsupportedApiHooks(parser, compilation)
229230
}
230231
}
231232

@@ -454,9 +455,78 @@ function getEntryFiles(entryFiles: string[], meta: EntryMetadata) {
454455
return files
455456
}
456457

457-
function isUserMiddlewareUserFile(module: any) {
458+
function registerUnsupportedApiHooks(
459+
parser: webpack5.javascript.JavascriptParser,
460+
compilation: webpack5.Compilation
461+
) {
462+
const { WebpackError } = compilation.compiler.webpack
463+
for (const expression of EDGE_UNSUPPORTED_NODE_APIS) {
464+
const warnForUnsupportedApi = (node: any) => {
465+
if (!isInMiddlewareLayer(parser)) {
466+
return
467+
}
468+
compilation.warnings.push(
469+
makeUnsupportedApiError(WebpackError, parser, expression, node.loc)
470+
)
471+
return true
472+
}
473+
parser.hooks.call.for(expression).tap(NAME, warnForUnsupportedApi)
474+
parser.hooks.expression.for(expression).tap(NAME, warnForUnsupportedApi)
475+
parser.hooks.callMemberChain
476+
.for(expression)
477+
.tap(NAME, warnForUnsupportedApi)
478+
parser.hooks.expressionMemberChain
479+
.for(expression)
480+
.tap(NAME, warnForUnsupportedApi)
481+
}
482+
483+
const warnForUnsupportedProcessApi = (node: any, [callee]: string[]) => {
484+
if (!isInMiddlewareLayer(parser) || callee === 'env') {
485+
return
486+
}
487+
compilation.warnings.push(
488+
makeUnsupportedApiError(
489+
WebpackError,
490+
parser,
491+
`process.${callee}`,
492+
node.loc
493+
)
494+
)
495+
return true
496+
}
497+
498+
parser.hooks.callMemberChain
499+
.for('process')
500+
.tap(NAME, warnForUnsupportedProcessApi)
501+
parser.hooks.expressionMemberChain
502+
.for('process')
503+
.tap(NAME, warnForUnsupportedProcessApi)
504+
}
505+
506+
function makeUnsupportedApiError(
507+
WebpackError: typeof webpack5.WebpackError,
508+
parser: webpack5.javascript.JavascriptParser,
509+
name: string,
510+
loc: any
511+
) {
512+
const error = new WebpackError(
513+
`You're using a Node.js API (${name} at line: ${loc.start.line}) which is not supported in the Edge Runtime that Middleware uses.
514+
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`
515+
)
516+
error.name = NAME
517+
error.module = parser.state.current
518+
error.loc = loc
519+
return error
520+
}
521+
522+
function isInMiddlewareLayer(parser: webpack5.javascript.JavascriptParser) {
523+
return parser.state.module?.layer === 'middleware'
524+
}
525+
526+
function isInMiddlewareFile(parser: webpack5.javascript.JavascriptParser) {
458527
return (
459-
module.layer === 'middleware' && /middleware\.\w+$/.test(module.rawRequest)
528+
parser.state.current?.layer === 'middleware' &&
529+
/middleware\.\w+$/.test(parser.state.current?.rawRequest)
460530
)
461531
}
462532

packages/next/server/web/sandbox/context.ts

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from 'next/dist/compiled/abort-controller'
1212
import vm from 'vm'
1313
import type { WasmBinding } from '../../../build/webpack/loaders/get-module-build-info'
14+
import { EDGE_UNSUPPORTED_NODE_APIS } from '../../../shared/lib/constants'
1415

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

51+
interface ModuleContextOptions {
52+
module: string
53+
onWarning: (warn: Error) => void
54+
useCache: boolean
55+
env: string[]
56+
wasm: WasmBinding[]
57+
}
58+
5059
/**
5160
* For a given module name this function will create a context for the
5261
* runtime. It returns a function where we can provide a module path and
5362
* run in within the context. It may or may not use a cache depending on
5463
* the parameters.
5564
*/
56-
export async function getModuleContext(options: {
57-
module: string
58-
onWarning: (warn: Error) => void
59-
useCache: boolean
60-
env: string[]
61-
wasm: WasmBinding[]
62-
}) {
65+
export async function getModuleContext(options: ModuleContextOptions) {
6366
let moduleCache = options.useCache
6467
? caches.get(options.module)
6568
: await createModuleContext(options)
@@ -97,12 +100,7 @@ export async function getModuleContext(options: {
97100
* 2. Dependencies that require runtime globals such as Blob.
98101
* 3. Dependencies that are scoped for the provided parameters.
99102
*/
100-
async function createModuleContext(options: {
101-
onWarning: (warn: Error) => void
102-
module: string
103-
env: string[]
104-
wasm: WasmBinding[]
105-
}) {
103+
async function createModuleContext(options: ModuleContextOptions) {
106104
const requireCache = new Map([
107105
[require.resolve('next/dist/compiled/cookie'), { exports: cookie }],
108106
])
@@ -181,11 +179,10 @@ async function createModuleContext(options: {
181179
* Create a base context with all required globals for the runtime that
182180
* won't depend on any externally provided dependency.
183181
*/
184-
function createContext(options: {
185-
/** Environment variables to be provided to the context */
186-
env: string[]
187-
}) {
188-
const context: { [key: string]: unknown } = {
182+
function createContext(
183+
options: Pick<ModuleContextOptions, 'env' | 'onWarning'>
184+
) {
185+
const context: Context = {
189186
_ENTRIES: {},
190187
atob: polyfills.atob,
191188
Blob,
@@ -207,19 +204,20 @@ function createContext(options: {
207204
CryptoKey: polyfills.CryptoKey,
208205
Crypto: polyfills.Crypto,
209206
crypto: new polyfills.Crypto(),
207+
DataView,
210208
File,
211209
FormData,
212-
process: {
213-
env: buildEnvironmentVariablesFrom(options.env),
214-
},
210+
process: createProcessPolyfill(options),
215211
ReadableStream,
216212
setInterval,
217213
setTimeout,
214+
queueMicrotask,
218215
TextDecoder,
219216
TextEncoder,
220217
TransformStream,
221218
URL,
222219
URLSearchParams,
220+
WebAssembly,
223221

224222
// Indexed collections
225223
Array,
@@ -244,6 +242,18 @@ function createContext(options: {
244242
// Structured data
245243
ArrayBuffer,
246244
SharedArrayBuffer,
245+
246+
// These APIs are supported by the Edge runtime, but not by the version of Node.js we're using
247+
// Since we'll soon replace this sandbox with the edge-runtime itself, it's not worth polyfilling.
248+
// ReadableStreamBYOBReader,
249+
// ReadableStreamDefaultReader,
250+
// structuredClone,
251+
// SubtleCrypto,
252+
// WritableStream,
253+
// WritableStreamDefaultWriter,
254+
}
255+
for (const name of EDGE_UNSUPPORTED_NODE_APIS) {
256+
addStub(context, name, options)
247257
}
248258

249259
// Self references
@@ -286,3 +296,57 @@ async function loadWasm(
286296

287297
return modules
288298
}
299+
300+
function createProcessPolyfill(
301+
options: Pick<ModuleContextOptions, 'env' | 'onWarning'>
302+
) {
303+
const env = buildEnvironmentVariablesFrom(options.env)
304+
305+
const processPolyfill = { env }
306+
const overridenValue: Record<string, any> = {}
307+
for (const key of Object.keys(process)) {
308+
if (key === 'env') continue
309+
Object.defineProperty(processPolyfill, key, {
310+
get() {
311+
emitWarning(`process.${key}`, options)
312+
return overridenValue[key]
313+
},
314+
set(value) {
315+
overridenValue[key] = value
316+
},
317+
enumerable: false,
318+
})
319+
}
320+
return processPolyfill
321+
}
322+
323+
const warnedAlready = new Set<string>()
324+
325+
function addStub(
326+
context: Context,
327+
name: string,
328+
contextOptions: Pick<ModuleContextOptions, 'onWarning'>
329+
) {
330+
Object.defineProperty(context, name, {
331+
get() {
332+
emitWarning(name, contextOptions)
333+
return undefined
334+
},
335+
enumerable: false,
336+
})
337+
}
338+
339+
function emitWarning(
340+
name: string,
341+
contextOptions: Pick<ModuleContextOptions, 'onWarning'>
342+
) {
343+
if (!warnedAlready.has(name)) {
344+
const warning =
345+
new Error(`You're using a Node.js API (${name}) which is not supported in the Edge Runtime that Middleware uses.
346+
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
347+
warning.name = 'NodejsRuntimeApiInMiddlewareWarning'
348+
contextOptions.onWarning(warning)
349+
console.warn(warning.message)
350+
warnedAlready.add(name)
351+
}
352+
}

packages/next/shared/lib/constants.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,31 @@ export const OPTIMIZED_FONT_PROVIDERS = [
6868
]
6969
export const STATIC_STATUS_PAGES = ['/500']
7070
export const TRACE_OUTPUT_VERSION = 1
71+
72+
// comparing
73+
// https://nextjs.org/docs/api-reference/edge-runtime
74+
// with
75+
// https://nodejs.org/docs/latest/api/globals.html
76+
export const EDGE_UNSUPPORTED_NODE_APIS = [
77+
'clearImmediate',
78+
'setImmediate',
79+
'BroadcastChannel',
80+
'Buffer',
81+
'ByteLengthQueuingStrategy',
82+
'CompressionStream',
83+
'CountQueuingStrategy',
84+
'DecompressionStream',
85+
'DomException',
86+
'Event',
87+
'EventTarget',
88+
'MessageChannel',
89+
'MessageEvent',
90+
'MessagePort',
91+
'ReadableByteStreamController',
92+
'ReadableStreamBYOBRequest',
93+
'ReadableStreamDefaultController',
94+
'TextDecoderStream',
95+
'TextEncoderStream',
96+
'TransformStreamDefaultController',
97+
'WritableStreamDefaultController',
98+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default function middleware() {
2+
process.cwd = () => 'fixed-value'
3+
console.log(process.cwd(), process.env)
4+
return new Response()
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Home() {
2+
return <div>A page</div>
3+
}

0 commit comments

Comments
 (0)