Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fall back to default components when a top-level error occurs #24079

Merged
merged 3 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 38 additions & 39 deletions packages/next/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,50 +254,49 @@ export default async (opts: { webpackHMR?: any } = {}) => {
webpackHMR = opts.webpackHMR
}

const appEntrypoint = await pageLoader.routeLoader.whenEntrypoint('/_app')
if ('error' in appEntrypoint) {
throw appEntrypoint.error
}
let initialErr = hydrateErr

const { component: app, exports: mod } = appEntrypoint
CachedApp = app as AppComponent

if (mod && mod.reportWebVitals) {
onPerfEntry = ({
id,
name,
startTime,
value,
duration,
entryType,
entries,
}): void => {
// Combines timestamp with random number for unique ID
const uniqueID: string = `${Date.now()}-${
Math.floor(Math.random() * (9e12 - 1)) + 1e12
}`
let perfStartEntry: string | undefined

if (entries && entries.length) {
perfStartEntry = entries[0].startTime
}
try {
const appEntrypoint = await pageLoader.routeLoader.whenEntrypoint('/_app')
if ('error' in appEntrypoint) {
throw appEntrypoint.error
}

mod.reportWebVitals({
id: id || uniqueID,
const { component: app, exports: mod } = appEntrypoint
CachedApp = app as AppComponent
if (mod && mod.reportWebVitals) {
onPerfEntry = ({
id,
name,
startTime: startTime || perfStartEntry,
value: value == null ? duration : value,
label:
entryType === 'mark' || entryType === 'measure'
? 'custom'
: 'web-vital',
})
}
}
startTime,
value,
duration,
entryType,
entries,
}): void => {
// Combines timestamp with random number for unique ID
const uniqueID: string = `${Date.now()}-${
Math.floor(Math.random() * (9e12 - 1)) + 1e12
}`
let perfStartEntry: string | undefined

if (entries && entries.length) {
perfStartEntry = entries[0].startTime
}

let initialErr = hydrateErr
mod.reportWebVitals({
id: id || uniqueID,
name,
startTime: startTime || perfStartEntry,
value: value == null ? duration : value,
label:
entryType === 'mark' || entryType === 'measure'
? 'custom'
: 'web-vital',
})
}
}

try {
const pageEntrypoint =
// The dev server fails to serve script assets when there's a hydration
// error, so we need to skip waiting for the entrypoint.
Expand Down
16 changes: 16 additions & 0 deletions packages/next/next-server/server/load-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ export type LoadComponentsReturnType = {
ComponentMod: any
}

export async function loadDefaultErrorComponents(distDir: string) {
const Document = interopDefault(require('next/dist/pages/_document'))
const App = interopDefault(require('next/dist/pages/_app'))
const ComponentMod = require('next/dist/pages/_error')
const Component = interopDefault(ComponentMod)

return {
App,
Document,
Component,
buildManifest: require(join(distDir, BUILD_MANIFEST)),
reactLoadableManifest: require(join(distDir, REACT_LOADABLE_MANIFEST)),
ComponentMod,
}
}

export async function loadComponents(
distDir: string,
pathname: string,
Expand Down
75 changes: 48 additions & 27 deletions packages/next/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ import {
import { DomainLocales, isTargetLikeServerless, NextConfig } from './config'
import pathMatch from '../lib/router/utils/path-match'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
import { loadComponents, LoadComponentsReturnType } from './load-components'
import {
loadComponents,
LoadComponentsReturnType,
loadDefaultErrorComponents,
} from './load-components'
import { normalizePagePath } from './normalize-page-path'
import { RenderOpts, RenderOptsPartial, renderToHTML } from './render'
import { getPagePath, requireFontManifest } from './require'
Expand Down Expand Up @@ -1964,38 +1968,38 @@ export default class Server {
_pathname: string,
query: ParsedUrlQuery = {}
) {
let result: null | FindComponentsResult = null
let html: string | null
try {
let result: null | FindComponentsResult = null

const is404 = res.statusCode === 404
let using404Page = false
const is404 = res.statusCode === 404
let using404Page = false

// use static 404 page if available and is 404 response
if (is404) {
result = await this.findPageComponents('/404', query)
using404Page = result !== null
}
let statusPage = `/${res.statusCode}`
// use static 404 page if available and is 404 response
if (is404) {
result = await this.findPageComponents('/404', query)
using404Page = result !== null
}
let statusPage = `/${res.statusCode}`

if (!result && STATIC_STATUS_PAGES.includes(statusPage)) {
result = await this.findPageComponents(statusPage, query)
}
if (!result && STATIC_STATUS_PAGES.includes(statusPage)) {
result = await this.findPageComponents(statusPage, query)
}

if (!result) {
result = await this.findPageComponents('/_error', query)
statusPage = '/_error'
}
if (!result) {
result = await this.findPageComponents('/_error', query)
statusPage = '/_error'
}

if (
process.env.NODE_ENV !== 'production' &&
!using404Page &&
(await this.hasPage('/_error')) &&
!(await this.hasPage('/404'))
) {
this.customErrorNo404Warn()
}
if (
process.env.NODE_ENV !== 'production' &&
!using404Page &&
(await this.hasPage('/_error')) &&
!(await this.hasPage('/404'))
) {
this.customErrorNo404Warn()
}

let html: string | null
try {
try {
html = await this.renderToHTMLWithComponents(
req,
Expand All @@ -2016,6 +2020,23 @@ export default class Server {
} catch (renderToHtmlError) {
console.error(renderToHtmlError)
res.statusCode = 500

if (this.renderOpts.dev) {
const fallbackResult = await loadDefaultErrorComponents(this.distDir)
return this.renderToHTMLWithComponents(
req,
res,
'/_error',
{
query,
components: fallbackResult,
},
{
...this.renderOpts,
err,
}
)
}
html = 'Internal Server Error'
}
return html
Expand Down
41 changes: 41 additions & 0 deletions test/acceptance/ReactRefreshLogBox.dev.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1260,3 +1260,44 @@ test('<Link> component props errors', async () => {

await cleanup()
})

test('_app top level error shows logbox', async () => {
const [session, cleanup] = await sandbox(
undefined,
new Map([
[
'pages/_app.js',
`
throw new Error("test");
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp
`,
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"pages/_app.js (2:16) @ eval

1 |
> 2 | throw new Error(\\"test\\");
| ^
3 | function MyApp({ Component, pageProps }) {
4 | return <Component {...pageProps} />;
5 | }"
`)

await session.patch(
'pages/_app.js',
`
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp
`
)
expect(await session.hasRedbox()).toBe(false)
await cleanup()
})