Skip to content

Commit

Permalink
Fall back to default components when a top-level error occurs (vercel…
Browse files Browse the repository at this point in the history
…#24079)

This fixes an internal server error showing when a top-level error occurs in `_app` in development instead of the dev overlay. This includes the failing test case from vercel#24069 and also ensures the overlay is cleared when the error is corrected. 

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added

Fixes: vercel#24070
Closes: vercel#24069
  • Loading branch information
ijjk authored and SokratisVidros committed Apr 20, 2021
1 parent d0dc6b0 commit 01f0a96
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 66 deletions.
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()
})

0 comments on commit 01f0a96

Please sign in to comment.