diff --git a/errors/getinitialprops-enoent.md b/errors/getinitialprops-enoent.md new file mode 100644 index 000000000000..eacf8c2f12cd --- /dev/null +++ b/errors/getinitialprops-enoent.md @@ -0,0 +1,16 @@ +# ENOENT from getInitialProps + +#### Why This Error Occurred + +In one of your pages you threw an error with the code `ENOENT`, this was previously an internal feature used to trigger rendering the 404 page and should not be relied on. + +#### Possible Ways to Fix It + +In `getServerSideProps` or `getInitialProps` you can set the status code with the ServerResponse (`res`) object provided e.g. `res.statusCode = 404`. + +In `getStaticProps` you can handle a 404 by rendering the 404 state on the current page instead of throwing and trying to render a separate page. + +### Useful Links + +- [Google 404 Error Guide](https://developers.google.com/search/docs/guides/fix-search-javascript) +- [404 Page Documentation](https://nextjs.org/docs/advanced-features/custom-error-page#404-page) diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 0a22081d29d7..7c39775627c1 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -1245,9 +1245,14 @@ export default class Server { } } } catch (err) { - this.logError(err) - res.statusCode = 500 - return await this.renderErrorToHTML(err, req, res, pathname, query) + if (err && err.code === 'ENOENT') { + res.statusCode = 404 + return await this.renderErrorToHTML(null, req, res, pathname, query) + } else { + this.logError(err) + res.statusCode = 500 + return await this.renderErrorToHTML(err, req, res, pathname, query) + } } res.statusCode = 404 diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 40257d88b605..3c296ae55437 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -42,6 +42,7 @@ import { LoadComponentsReturnType, ManifestItem } from './load-components' import optimizeAmp from './optimize-amp' import { UnwrapPromise } from '../../lib/coalesced-function' import { GetStaticProps, GetServerSideProps } from '../../types' +import chalk from 'next/dist/compiled/chalk' function noRouter() { const message = @@ -637,7 +638,14 @@ export async function renderToHTML( ;(renderOpts as any).pageData = props } } catch (err) { - if (isDataReq || !dev || !err) throw err + if (process.env.NODE_ENV !== 'production' && err?.code === 'ENOENT') { + console.warn( + chalk.yellow('Warning:') + + ` page "${pathname}" threw an error with a code of ENOENT. This was an internal feature to render the 404 page and should not be relied on.\nSee more info here: https://err.sh/next.js/getinitialprops-enoent` + ) + } + + if (isDataReq || !dev || !err || err.code === 'ENOENT') throw err ctx.err = err renderOpts.err = err console.error(err) diff --git a/test/integration/custom-error/pages/throw-404.js b/test/integration/custom-error/pages/throw-404.js new file mode 100644 index 000000000000..889a45e4f7f0 --- /dev/null +++ b/test/integration/custom-error/pages/throw-404.js @@ -0,0 +1,9 @@ +const Page = () => 'hi' + +Page.getInitialProps = () => { + const error = new Error('to 404 we go') + error.code = 'ENOENT' + throw error +} + +export default Page diff --git a/test/integration/custom-error/test/index.test.js b/test/integration/custom-error/test/index.test.js index b58df310cb4d..f9cf0934fb08 100644 --- a/test/integration/custom-error/test/index.test.js +++ b/test/integration/custom-error/test/index.test.js @@ -14,13 +14,27 @@ import { jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 const appDir = join(__dirname, '..') const nextConfig = join(appDir, 'next.config.js') + +let stderr = '' let appPort let app -const runTests = () => { +const runTests = isDev => { it('renders custom _error successfully', async () => { const html = await renderViaHTTP(appPort, '/') - expect(html).toMatch(/Custom error/) + expect(html).toMatch(isDev ? /oof/ : /Custom error/) + }) + + it('renders 404 page when ENOENT error is thrown', async () => { + const html = await renderViaHTTP(appPort, '/throw-404') + expect(html).toContain('Custom error') + expect(html).not.toContain('to 404 we go') + + if (isDev) { + expect(stderr).toContain( + 'page "/throw-404" threw an error with a code of ENOENT. This was an internal feature to render the 404 page and should not be relied on' + ) + } }) } @@ -28,9 +42,8 @@ const customErrNo404Match = /You have added a custom \/_error page without a cus describe('Custom _error', () => { describe('dev mode', () => { - let stderr = '' - beforeAll(async () => { + stderr = '' appPort = await findPort() app = await launchApp(appDir, appPort, { onStderr(msg) { @@ -56,6 +69,8 @@ describe('Custom _error', () => { expect(html).toContain('An error 404 occurred on server') expect(stderr).toMatch(customErrNo404Match) }) + + runTests(true) }) describe('production mode', () => { diff --git a/test/integration/prerender/pages/enoent.js b/test/integration/prerender/pages/enoent.js new file mode 100644 index 000000000000..a8f9ce6ae151 --- /dev/null +++ b/test/integration/prerender/pages/enoent.js @@ -0,0 +1,14 @@ +export async function getStaticProps() { + if (process.env.NODE_ENV === 'development') { + const error = new Error('oof') + error.code = 'ENOENT' + throw error + } + return { + props: { + hi: 'hi', + }, + } +} + +export default () => 'hi' diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index 923c2d65558b..21d08b8673c2 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -129,6 +129,11 @@ const expectedManifestRoutes = () => ({ initialRevalidateSeconds: false, srcRoute: null, }, + '/enoent': { + dataRoute: `/_next/data/${buildId}/enoent.json`, + initialRevalidateSeconds: false, + srcRoute: null, + }, '/something': { dataRoute: `/_next/data/${buildId}/something.json`, initialRevalidateSeconds: false, @@ -505,6 +510,12 @@ const runTests = (dev = false, looseMode = false) => { // ) // }) + it('should handle throw ENOENT correctly', async () => { + const res = await fetchViaHTTP(appPort, '/enoent') + const html = await res.text() + expect(html).toContain('oof') + }) + it('should not show warning from url prop being returned', async () => { const urlPropPage = join(appDir, 'pages/url-prop.js') await fs.writeFile( @@ -810,6 +821,12 @@ const runTests = (dev = false, looseMode = false) => { ), page: '/default-revalidate', }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/enoent.json$` + ), + page: '/enoent', + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(