Skip to content

Commit

Permalink
sync access warnings for Request APIs should include a stack (#71519)
Browse files Browse the repository at this point in the history
This updates our logging to use Error objects to provide stacks to
better locate where sync access of Request APIs is happening
  • Loading branch information
gnoff authored Oct 19, 2024
1 parent d62627c commit dab4d00
Show file tree
Hide file tree
Showing 12 changed files with 67 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'

const errorRef: { current: null | string } = { current: null }
const errorRef: { current: null | Error } = { current: null }

// React.cache is currently only available in canary/experimental React channels.
const cache =
Expand Down Expand Up @@ -33,7 +33,7 @@ const flushCurrentErrorIfNew = cache(
* @returns
*/
export function createDedupedByCallsiteServerErrorLoggerDev<Args extends any[]>(
getMessage: (...args: Args) => string
getMessage: (...args: Args) => Error
) {
return function logDedupedError(...args: Args) {
const message = getMessage(...args)
Expand Down
12 changes: 6 additions & 6 deletions packages/next/src/server/request/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,10 +538,10 @@ const warnForSyncIteration = process.env
: createDedupedByCallsiteServerErrorLoggerDev(
function getSyncIterationMessage(route?: string) {
const prefix = route ? ` In route ${route} ` : ''
return (
return new Error(
`${prefix}cookies were iterated over. ` +
`\`cookies()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`cookies()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
)
Expand All @@ -553,10 +553,10 @@ const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS
expression: string
) {
const prefix = route ? ` In route ${route} a ` : 'A '
return (
return new Error(
`${prefix}cookie property was accessed directly with \`${expression}\`. ` +
`\`cookies()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`cookies()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
})

Expand Down
6 changes: 3 additions & 3 deletions packages/next/src/server/request/draft-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS
expression: string
) {
const prefix = route ? ` In route ${route} a ` : 'A '
return (
return new Error(
`${prefix}\`draftMode()\` property was accessed directly with \`${expression}\`. ` +
`\`draftMode()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/draft-mode-sync-access`
`\`draftMode()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/draft-mode-sync-access`
)
})

Expand Down
12 changes: 6 additions & 6 deletions packages/next/src/server/request/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,10 +460,10 @@ const warnForSyncIteration = process.env
: createDedupedByCallsiteServerErrorLoggerDev(
function getSyncIterationMessage(route?: string) {
const prefix = route ? ` In route ${route} ` : ''
return (
return new Error(
`${prefix}headers were iterated over. ` +
`\`headers()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`headers()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
)
Expand All @@ -475,10 +475,10 @@ const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS
expression: string
) {
const prefix = route ? ` In route ${route} a ` : 'A '
return (
return new Error(
`${prefix}header property was accessed directly with \`${expression}\`. ` +
`\`headers()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`headers()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
})

Expand Down
18 changes: 9 additions & 9 deletions packages/next/src/server/request/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,10 @@ const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS
expression: string
) {
const prefix = route ? ` In route ${route} a ` : 'A '
return (
return new Error(
`${prefix}param property was accessed directly with ${expression}. ` +
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
})

Expand All @@ -437,16 +437,16 @@ const warnForEnumeration = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS
if (missingProperties.length) {
const describedMissingProperties =
describeListOfPropertyNames(missingProperties)
return (
return new Error(
`${prefix}params are being enumerated incompletely missing these properties: ${describedMissingProperties}. ` +
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
} else {
return (
return new Error(
`${prefix}params are being enumerated. ` +
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
})
Expand Down
18 changes: 9 additions & 9 deletions packages/next/src/server/request/search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,10 +680,10 @@ const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS
expression: string
) {
const prefix = route ? ` In route ${route} a ` : 'A '
return (
return new Error(
`${prefix}searchParam property was accessed directly with ${expression}. ` +
`\`searchParams\` should be awaited before accessing properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`searchParams\` should be awaited before accessing properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
})

Expand All @@ -697,16 +697,16 @@ const warnForEnumeration = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS
if (missingProperties.length) {
const describedMissingProperties =
describeListOfPropertyNames(missingProperties)
return (
return new Error(
`${prefix}searchParams are being enumerated incompletely missing these properties: ${describedMissingProperties}. ` +
`\`searchParams\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`searchParams\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
} else {
return (
return new Error(
`${prefix}searchParams are being enumerated. ` +
`\`searchParams\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
`\`searchParams\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ describe('dynamic-requests warnings', () => {

const browser = await next.browser('/request/cookies')

const browserLogsserLogs = await browser.log()
const browserConsoleErrors = browserLogsserLogs
const browserLogs = await browser.log()
const browserConsoleErrors = browserLogs
.filter((log) => log.source === 'error')
.map((log) => log.message)
const terminalOutput = next.cliOutput.slice(nextDevBootstrapOutputIndex)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export default function Page({ params }) {
return <h1>Page {params.slug}</h1>
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>Page {slug}</h1>
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export default function Page({ params }) {
return <h1>Blog Sub Page {params.slug}</h1>
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>Blog Sub Page {slug}</h1>
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export function generateStaticParams() {
return [{ slug: 'first' }, { slug: 'second' }]
}

export const GET = (req: NextRequest, { params }) => {
return NextResponse.json({ params, now: Date.now() })
export const GET = async (
req: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) => {
const resolvedParams = await params
return NextResponse.json({ params: resolvedParams, now: Date.now() })
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export function generateStaticParams() {
return [{ slug: 'first' }, { slug: 'second' }]
}

export const GET = (req: NextRequest, { params }) => {
return NextResponse.json({ params, now: Date.now() })
export const GET = async (
req: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) => {
const resolvedParams = await params
return NextResponse.json({ params: resolvedParams, now: Date.now() })
}
6 changes: 4 additions & 2 deletions test/e2e/app-dir/app-routes/handlers/hello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import { withRequestMeta } from '../helpers'

const helloHandler = async (
request: NextRequest,
{ params }: { params?: Record<string, string | string[]> }
{ params }: { params?: Promise<Record<string, string | string[]>> }
): Promise<Response> => {
const { pathname } = request.nextUrl

if (typeof WebSocket === 'undefined') {
throw new Error('missing WebSocket constructor!!')
}

const resolvedParams = params ? await params : null

return new Response('hello, world', {
headers: withRequestMeta({
method: request.method,
params: params ?? null,
params: resolvedParams,
pathname,
}),
})
Expand Down

0 comments on commit dab4d00

Please sign in to comment.