Skip to content

Commit

Permalink
[metadata] set bypass ua regex string for ppr routes (#75977)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi authored and devjiwonchoi committed Feb 14, 2025
1 parent 798f4ce commit 36f2756
Show file tree
Hide file tree
Showing 22 changed files with 341 additions and 91 deletions.
6 changes: 1 addition & 5 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2771,11 +2771,7 @@ export default async function build(
},
// If it's PPR rendered non-static page, bypass the PPR cache when streaming metadata is enabled.
// This will skip the postpone data for those bots requests and instead produce a dynamic render.
...(isRoutePPREnabled &&
// Disable streaming metadata for PPR on deployment where we don't have the special env.
// TODO: enable streaming metadata in PPR mode by default once it's ready.
process.env.__NEXT_EXPERIMENTAL_PPR === 'true' &&
config.experimental.streamingMetadata
...(isRoutePPREnabled && config.experimental.streamingMetadata
? [
{
type: 'header',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const consoleTypeSym = Symbol.for('next.console.error.type')
type UnhandledError = Error & {
[digestSym]: 'NEXT_UNHANDLED_ERROR'
[consoleTypeSym]: 'string' | 'error'
environmentName: string
}

export function createUnhandledError(message: string | Error): UnhandledError {
Expand Down
16 changes: 14 additions & 2 deletions packages/next/src/client/components/errors/use-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { useEffect } from 'react'
import { attachHydrationErrorState } from './attach-hydration-error-state'
import { isNextRouterError } from '../is-next-router-error'
import { storeHydrationErrorStateFromConsoleArgs } from './hydration-error-info'
import { formatConsoleArgs } from '../../lib/console'
import { formatConsoleArgs, parseConsoleArgs } from '../../lib/console'
import isError from '../../../lib/is-error'
import { createUnhandledError } from './console-error'
import {
createUnhandledError,
isUnhandledConsoleOrRejection,
} from './console-error'
import { enqueueConsecutiveDedupedError } from './enqueue-client-error'
import { getReactStitchedError } from '../errors/stitched-error'

Expand Down Expand Up @@ -35,6 +38,15 @@ export function handleClientError(
}
error = getReactStitchedError(error)

if (isUnhandledConsoleOrRejection(error)) {
if (!error.environmentName) {
const { environmentName } = parseConsoleArgs(consoleErrorArgs)
if (environmentName) {
error.environmentName = environmentName
}
}
}

storeHydrationErrorStateFromConsoleArgs(...consoleErrorArgs)
attachHydrationErrorState(error)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import isError from '../../../lib/is-error'
import { isNextRouterError } from '../is-next-router-error'
import { handleClientError } from '../errors/use-error-handler'
import { parseConsoleArgs } from '../../lib/console'

export const originConsoleError = window.console.error

Expand All @@ -13,7 +14,7 @@ export function patchConsoleError() {
window.console.error = function error(...args: any[]) {
let maybeError: unknown
if (process.env.NODE_ENV !== 'production') {
const replayedError = matchReplayedError(...args)
const { error: replayedError } = parseConsoleArgs(args)
if (replayedError) {
maybeError = replayedError
} else if (isError(args[0])) {
Expand Down Expand Up @@ -41,34 +42,3 @@ export function patchConsoleError() {
}
}
}

function matchReplayedError(...args: unknown[]): Error | null {
// See
// https://github.com/facebook/react/blob/65a56d0e99261481c721334a3ec4561d173594cd/packages/react-devtools-shared/src/backend/flight/renderer.js#L88-L93
//
// Logs replayed from the server look like this:
// [
// "%c%s%c %o\n\n%s\n\n%s\n",
// "background: #e6e6e6; ...",
// " Server ", // can also be e.g. " Prerender "
// "",
// Error
// "The above error occurred in the <Page> component."
// ...
// ]
if (
args.length > 3 &&
typeof args[0] === 'string' &&
args[0].startsWith('%c%s%c ') &&
typeof args[1] === 'string' &&
typeof args[2] === 'string' &&
typeof args[3] === 'string'
) {
const maybeError = args[4]
if (isError(maybeError)) {
return maybeError
}
}

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/react'
import { EnvironmentNameLabel } from './environment-name-label'
import { withShadowPortal } from '../../../storybook/with-shadow-portal'

const meta: Meta<typeof EnvironmentNameLabel> = {
component: EnvironmentNameLabel,
parameters: {
layout: 'centered',
},
decorators: [withShadowPortal],
}

export default meta
type Story = StoryObj<typeof EnvironmentNameLabel>

export const Server: Story = {
args: {
environmentName: 'Server',
},
}

export const Prerender: Story = {
args: {
environmentName: 'Prerender',
},
}

export const Cache: Story = {
args: {
environmentName: 'Cache',
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { noop as css } from '../../../helpers/noop-template'

export function EnvironmentNameLabel({
environmentName,
}: {
environmentName: string
}) {
return <span data-nextjs-environment-name-label>{environmentName}</span>
}

export const ENVIRONMENT_NAME_LABEL_STYLES = css`
[data-nextjs-environment-name-label] {
padding: var(--size-0_5) var(--size-1_5);
margin: 0;
/* used --size instead of --rounded because --rounded is missing 6px */
border-radius: var(--size-1_5);
background: var(--color-gray-300);
font-weight: 600;
font-size: var(--size-font-11);
color: var(--color-gray-900);
font-family: var(--font-stack-monospace);
line-height: var(--size-5);
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ import { OVERLAY_STYLES, ErrorOverlayOverlay } from '../overlay/overlay'
import { ErrorOverlayBottomStack } from '../error-overlay-bottom-stack'
import type { ErrorBaseProps } from '../error-overlay/error-overlay'
import type { ReadyRuntimeError } from '../../../../../internal/helpers/get-error-by-type'
import { EnvironmentNameLabel } from '../environment-name-label/environment-name-label'

interface ErrorOverlayLayoutProps extends ErrorBaseProps {
errorMessage: ErrorMessageType
errorType: ErrorType
children?: React.ReactNode
errorCode?: string
error: Error
error: ReadyRuntimeError['error']
debugInfo?: DebugInfo
isBuildError?: boolean
onClose?: () => void
Expand Down Expand Up @@ -97,7 +98,14 @@ export function ErrorOverlayLayout({
// allow assertion in tests before error rating is implemented
data-nextjs-error-code={errorCode}
>
<ErrorTypeLabel errorType={errorType} />
<span data-nextjs-error-label-group>
<ErrorTypeLabel errorType={errorType} />
{error.environmentName && (
<EnvironmentNameLabel
environmentName={error.environmentName}
/>
)}
</span>
<ErrorOverlayToolbar error={error} debugInfo={debugInfo} />
</div>
<ErrorMessage errorMessage={errorMessage} />
Expand Down Expand Up @@ -141,4 +149,10 @@ export const styles = css`
${errorMessageStyles}
${toolbarStyles}
${CALL_STACK_STYLES}
[data-nextjs-error-label-group] {
display: flex;
align-items: center;
gap: var(--size-2);
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const styles = css`
margin: 0;
/* used --size instead of --rounded because --rounded is missing 6px */
border-radius: var(--size-1_5);
background: var(--color-red-100);
background: var(--color-red-300);
font-weight: 600;
font-size: var(--size-font-11);
color: var(--color-red-900);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,22 @@ function ErrorDescription({
? ''
: error.name + ': '

// If it's replayed error, display the environment name
const environmentName =
'environmentName' in error ? error['environmentName'] : ''
'environmentName' in error ? error.environmentName : ''
const envPrefix = environmentName ? `[ ${environmentName} ] ` : ''

// The environment name will be displayed as a label, so remove it
// from the message (e.g. "[ Server ] hello world" -> "hello world").
let message = error.message
if (message.startsWith(envPrefix)) {
message = message.slice(envPrefix.length)
}

return (
<>
{envPrefix}
{title}
<HotlinkedText
text={hydrationWarning || error.message}
text={hydrationWarning || message}
matcher={isNextjsLink}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import { CALL_STACK_FRAME_STYLES } from '../components/call-stack-frame/call-sta
import { styles as devToolsIndicator } from '../components/errors/dev-tools-indicator/styles'
import { noop as css } from '../helpers/noop-template'
import { EDITOR_LINK_STYLES } from '../components/terminal/editor-link'

import { ENVIRONMENT_NAME_LABEL_STYLES } from '../components/errors/environment-name-label/environment-name-label'
export function ComponentStyles() {
return (
<style>
{css`
${COPY_BUTTON_STYLES}
${CALL_STACK_FRAME_STYLES}
${ENVIRONMENT_NAME_LABEL_STYLES}
${overlay}
${toast}
${dialog}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ function ErrorDescription({
const environmentName =
'environmentName' in error ? error['environmentName'] : ''
const envPrefix = environmentName ? `[ ${environmentName} ] ` : ''
const isMsgMissingEnvPrefix = !error.message.startsWith(envPrefix)
return (
<>
{envPrefix}
{isMsgMissingEnvPrefix && envPrefix}
{title}
<HotlinkedText
text={hydrationWarning || error.message}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import React from 'react'
export type ReadyRuntimeError = {
id: number
runtime: true
error: Error
error: Error & { environmentName?: string }
frames: OriginalStackFrame[] | (() => Promise<OriginalStackFrame[]>)
componentStackFrames?: ComponentStackFrame[]
}
Expand Down
42 changes: 42 additions & 0 deletions packages/next/src/client/lib/console.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import isError from '../../lib/is-error'

function formatObject(arg: unknown, depth: number) {
switch (typeof arg) {
case 'object':
Expand Down Expand Up @@ -109,3 +111,43 @@ export function formatConsoleArgs(args: unknown[]): string {

return result
}

export function parseConsoleArgs(args: unknown[]): {
environmentName: string | null
error: Error | null
} {
// See
// https://github.com/facebook/react/blob/65a56d0e99261481c721334a3ec4561d173594cd/packages/react-devtools-shared/src/backend/flight/renderer.js#L88-L93
//
// Logs replayed from the server look like this:
// [
// "%c%s%c %o\n\n%s\n\n%s\n",
// "background: #e6e6e6; ...",
// " Server ", // can also be e.g. " Prerender "
// "",
// Error
// "The above error occurred in the <Page> component."
// ...
// ]
if (
args.length > 3 &&
typeof args[0] === 'string' &&
args[0].startsWith('%c%s%c ') &&
typeof args[1] === 'string' &&
typeof args[2] === 'string' &&
typeof args[3] === 'string'
) {
const environmentName = args[2]
const maybeError = args[4]

return {
environmentName: environmentName.trim(),
error: isError(maybeError) ? maybeError : null,
}
}

return {
environmentName: null,
error: null,
}
}
13 changes: 10 additions & 3 deletions test/development/acceptance-app/dynamic-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,15 @@ describe('dynamic = "error" in devmode', () => {
)
const { session } = sandbox
await session.assertHasRedbox()
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"[ Server ] Error: Route /server with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering"`
)
const description = await session.getRedboxDescription()
if (process.env.__NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY === 'true') {
expect(description).toMatchInlineSnapshot(
`"Error: Route /server with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering"`
)
} else {
expect(description).toMatchInlineSnapshot(
`"[ Server ] Error: Route /server with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering"`
)
}
})
})
Loading

0 comments on commit 36f2756

Please sign in to comment.