Skip to content

Commit 34f6f4a

Browse files
committed
Remove proxy for cookies and headers to improve awaited value detection.
The proxy obscures the awaited value for the suspended by information for await cookies() and await headers(). Instead we use property descriptors that are revokable. This allows the debug info to convey the actual awaited value. We cannot do the same for params and searchParams because for those we need the proxy to trap arbitrary param and searchParam sync access. We can remove this in a future version of Next.js and then the awaited values will improve in dev
1 parent e8018c1 commit 34f6f4a

File tree

3 files changed

+116
-58
lines changed

3 files changed

+116
-58
lines changed

packages/next/errors.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,5 +888,5 @@
888888
"887": "`cacheLife()` is only available with the `cacheComponents` config.",
889889
"888": "Unknown \\`cacheLife()\\` profile \"%s\" is not configured in next.config.js\\nmodule.exports = {\n cacheLife: {\n \"%s\": ...\\n }\n}",
890890
"889": "Unknown \\`cacheLife()\\` profile \"%s\" is not configured in next.config.js\\nmodule.exports = {\n cacheLife: {\n \"%s\": ...\\n }\n}",
891-
"890": "Received a underlying cookies object that does not match either `cookies` or `mutableCookies`"
891+
"890": "Received an underlying cookies object that does not match either `cookies` or `mutableCookies`"
892892
}

packages/next/src/server/request/cookies.ts

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger'
2929
import { isRequestAPICallableInsideAfter } from './utils'
3030
import { InvariantError } from '../../shared/lib/invariant-error'
31-
import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
3231
import { RenderStage } from '../app-render/staged-rendering'
3332

3433
export function cookies(): Promise<ReadonlyRequestCookies> {
@@ -198,7 +197,7 @@ function makeUntrackedCookiesWithDevWarnings(
198197
promise = requestStore.asyncApiPromises.cookies
199198
} else {
200199
throw new InvariantError(
201-
'Received a underlying cookies object that does not match either `cookies` or `mutableCookies`'
200+
'Received an underlying cookies object that does not match either `cookies` or `mutableCookies`'
202201
)
203202
}
204203
return instrumentCookiesPromiseWithDevWarnings(promise, route)
@@ -230,35 +229,65 @@ function instrumentCookiesPromiseWithDevWarnings(
230229
promise: Promise<ReadonlyRequestCookies>,
231230
route: string | undefined
232231
) {
233-
return new Proxy(promise, {
234-
get(target, prop, receiver) {
235-
switch (prop) {
236-
case Symbol.iterator: {
237-
warnForSyncAccess(route, '`...cookies()` or similar iteration')
238-
break
239-
}
240-
case 'size':
241-
case 'get':
242-
case 'getAll':
243-
case 'has':
244-
case 'set':
245-
case 'delete':
246-
case 'clear':
247-
case 'toString': {
248-
warnForSyncAccess(route, `\`cookies().${prop}\``)
249-
break
250-
}
251-
default: {
252-
// We only warn for well-defined properties of the cookies object.
253-
}
254-
}
232+
Object.defineProperties(promise, {
233+
[Symbol.iterator]: replaceableWarningDescriptorForSymbolIterator(
234+
promise,
235+
route
236+
),
237+
size: replaceableWarningDescriptor(promise, 'size', route),
238+
get: replaceableWarningDescriptor(promise, 'get', route),
239+
getAll: replaceableWarningDescriptor(promise, 'getAll', route),
240+
has: replaceableWarningDescriptor(promise, 'has', route),
241+
set: replaceableWarningDescriptor(promise, 'set', route),
242+
delete: replaceableWarningDescriptor(promise, 'delete', route),
243+
clear: replaceableWarningDescriptor(promise, 'clear', route),
244+
toString: replaceableWarningDescriptor(promise, 'toString', route),
245+
})
246+
return promise
247+
}
255248

256-
return ReflectAdapter.get(target, prop, receiver)
249+
function replaceableWarningDescriptor(
250+
target: unknown,
251+
prop: string,
252+
route: string | undefined
253+
) {
254+
return {
255+
enumerable: false,
256+
get() {
257+
warnForSyncAccess(route, `\`cookies().${prop}\``)
258+
return undefined
257259
},
258-
set(target, prop, newValue, receiver) {
259-
return ReflectAdapter.set(target, prop, newValue, receiver)
260+
set(value: unknown) {
261+
Object.defineProperty(target, prop, {
262+
value,
263+
writable: true,
264+
configurable: true,
265+
})
260266
},
261-
})
267+
configurable: true,
268+
}
269+
}
270+
271+
function replaceableWarningDescriptorForSymbolIterator(
272+
target: unknown,
273+
route: string | undefined
274+
) {
275+
return {
276+
enumerable: false,
277+
get() {
278+
warnForSyncAccess(route, '`...cookies()` or similar iteration')
279+
return undefined
280+
},
281+
set(value: unknown) {
282+
Object.defineProperty(target, Symbol.iterator, {
283+
value,
284+
writable: true,
285+
enumerable: true,
286+
configurable: true,
287+
})
288+
},
289+
configurable: true,
290+
}
262291
}
263292

264293
function createCookiesAccessError(

packages/next/src/server/request/headers.ts

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger'
2727
import { isRequestAPICallableInsideAfter } from './utils'
2828
import { InvariantError } from '../../shared/lib/invariant-error'
29-
import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
3029
import { RenderStage } from '../app-render/staged-rendering'
3130

3231
/**
@@ -230,37 +229,67 @@ function instrumentHeadersPromiseWithDevWarnings(
230229
promise: Promise<ReadonlyHeaders>,
231230
route: string | undefined
232231
) {
233-
return new Proxy(promise, {
234-
get(target, prop, receiver) {
235-
switch (prop) {
236-
case Symbol.iterator: {
237-
warnForSyncAccess(route, '`...headers()` or similar iteration')
238-
break
239-
}
240-
case 'append':
241-
case 'delete':
242-
case 'get':
243-
case 'has':
244-
case 'set':
245-
case 'getSetCookie':
246-
case 'forEach':
247-
case 'keys':
248-
case 'values':
249-
case 'entries': {
250-
warnForSyncAccess(route, `\`headers().${prop}\``)
251-
break
252-
}
253-
default: {
254-
// We only warn for well-defined properties of the headers object.
255-
}
256-
}
232+
Object.defineProperties(promise, {
233+
[Symbol.iterator]: replaceableWarningDescriptorForSymbolIterator(
234+
promise,
235+
route
236+
),
237+
append: replaceableWarningDescriptor(promise, 'append', route),
238+
delete: replaceableWarningDescriptor(promise, 'delete', route),
239+
get: replaceableWarningDescriptor(promise, 'get', route),
240+
has: replaceableWarningDescriptor(promise, 'has', route),
241+
set: replaceableWarningDescriptor(promise, 'set', route),
242+
getSetCookie: replaceableWarningDescriptor(promise, 'getSetCookie', route),
243+
forEach: replaceableWarningDescriptor(promise, 'forEach', route),
244+
keys: replaceableWarningDescriptor(promise, 'keys', route),
245+
values: replaceableWarningDescriptor(promise, 'values', route),
246+
entries: replaceableWarningDescriptor(promise, 'entries', route),
247+
})
248+
return promise
249+
}
250+
251+
function replaceableWarningDescriptor(
252+
target: unknown,
253+
prop: string,
254+
route: string | undefined
255+
) {
256+
return {
257+
enumerable: false,
258+
get() {
259+
warnForSyncAccess(route, `\`headers().${prop}\``)
260+
return undefined
261+
},
262+
set(value: unknown) {
263+
Object.defineProperty(target, prop, {
264+
value,
265+
writable: true,
266+
configurable: true,
267+
})
268+
},
269+
configurable: true,
270+
}
271+
}
257272

258-
return ReflectAdapter.get(target, prop, receiver)
273+
function replaceableWarningDescriptorForSymbolIterator(
274+
target: unknown,
275+
route: string | undefined
276+
) {
277+
return {
278+
enumerable: false,
279+
get() {
280+
warnForSyncAccess(route, '`...headers()` or similar iteration')
281+
return undefined
259282
},
260-
set(target, prop, newValue, receiver) {
261-
return ReflectAdapter.set(target, prop, newValue, receiver)
283+
set(value: unknown) {
284+
Object.defineProperty(target, Symbol.iterator, {
285+
value,
286+
writable: true,
287+
enumerable: true,
288+
configurable: true,
289+
})
262290
},
263-
})
291+
configurable: true,
292+
}
264293
}
265294

266295
function createHeadersAccessError(

0 commit comments

Comments
 (0)