Skip to content

Commit

Permalink
Ensure default _app is used when falling back to default _error (#39467)
Browse files Browse the repository at this point in the history
* Ensure default _app is used when falling back to default _error

* make render check less specific due to arbitrary wait
  • Loading branch information
ijjk committed Aug 10, 2022
1 parent ed81a14 commit d64a150
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 5 deletions.
18 changes: 13 additions & 5 deletions packages/next/client/index.tsx
Expand Up @@ -468,7 +468,7 @@ async function render(renderingProps: RenderRouteInfo): Promise<void> {
// 404 and 500 errors are special kind of errors
// and they are still handle via the main render method.
function renderError(renderErrorProps: RenderErrorProps): Promise<any> {
const { App, err } = renderErrorProps
let { App, err } = renderErrorProps

// In development runtime errors are caught by our overlay
// In production we catch runtime errors using componentDidCatch which will trigger renderError
Expand Down Expand Up @@ -497,10 +497,18 @@ function renderError(renderErrorProps: RenderErrorProps): Promise<any> {
.loadPage('/_error')
.then(({ page: ErrorComponent, styleSheets }) => {
return lastAppProps?.Component === ErrorComponent
? import('../pages/_error').then((m) => ({
ErrorComponent: m.default as React.ComponentType<{}>,
styleSheets: [],
}))
? import('../pages/_error')
.then((errorModule) => {
return import('../pages/_app').then((appModule) => {
App = appModule.default as any as AppComponent
renderErrorProps.App = App
return errorModule
})
})
.then((m) => ({
ErrorComponent: m.default as React.ComponentType<{}>,
styleSheets: [],
}))
: { ErrorComponent, styleSheets }
})
.then(({ ErrorComponent, styleSheets }) => {
Expand Down
15 changes: 15 additions & 0 deletions test/production/fatal-render-errror/app/pages/_app.js
@@ -0,0 +1,15 @@
export default function App({ Component, pageProps }) {
if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined') {
if (!window.renderAttempts) {
window.renderAttempts = 0
}
window.renderAttempts++
throw new Error('error in custom _app')
}
return (
<>
<p id="custom-app">from _app</p>
<Component {...pageProps} />
</>
)
}
6 changes: 6 additions & 0 deletions test/production/fatal-render-errror/app/pages/_error.js
@@ -0,0 +1,6 @@
export default function Error() {
if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined') {
throw new Error('error in custom _app')
}
return <div>Error encountered!</div>
}
3 changes: 3 additions & 0 deletions test/production/fatal-render-errror/app/pages/index.js
@@ -0,0 +1,3 @@
export default function Page() {
return <p>index page</p>
}
6 changes: 6 additions & 0 deletions test/production/fatal-render-errror/app/pages/with-error.js
@@ -0,0 +1,6 @@
export default function Error() {
if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined') {
throw new Error('error in pages/with-error')
}
return <div>with-error</div>
}
55 changes: 55 additions & 0 deletions test/production/fatal-render-errror/index.test.ts
@@ -0,0 +1,55 @@
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { check, renderViaHTTP, waitFor } from 'next-test-utils'
import webdriver from 'next-webdriver'
import { join } from 'path'

describe('fatal-render-errror', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: new FileRef(join(__dirname, 'app')),
dependencies: {},
})
})
afterAll(() => next.destroy())

it('should render page without error correctly', async () => {
const html = await renderViaHTTP(next.url, '/')
expect(html).toContain('index page')
expect(html).toContain('from _app')
})

it('should handle fatal error in _app and _error without loop on direct visit', async () => {
const browser = await webdriver(next.url, '/with-error')

// wait a bit to see if we are rendering multiple times unexpectedly
await waitFor(500)
expect(await browser.eval('window.renderAttempts')).toBeLessThan(10)

const html = await browser.eval('document.documentElement.innerHTML')
expect(html).not.toContain('from _app')
expect(html).toContain(
'Application error: a client-side exception has occurred'
)
})

it('should handle fatal error in _app and _error without loop on client-transition', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.renderAttempts = 0')

await browser.eval('window.next.router.push("/with-error")')
await check(() => browser.eval('location.pathname'), '/with-error')

// wait a bit to see if we are rendering multiple times unexpectedly
await waitFor(500)
expect(await browser.eval('window.renderAttempts')).toBeLessThan(10)

const html = await browser.eval('document.documentElement.innerHTML')
expect(html).not.toContain('from _app')
expect(html).toContain(
'Application error: a client-side exception has occurred'
)
})
})

0 comments on commit d64a150

Please sign in to comment.