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

Ensure default _app is used when falling back to default _error #39467

Merged
merged 2 commits into from Aug 10, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'
)
})
})