Skip to content

Commit 272dd3e

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 e3276d4 commit 272dd3e

File tree

2 files changed

+114
-56
lines changed

2 files changed

+114
-56
lines changed

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

Lines changed: 56 additions & 27 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> {
@@ -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)