diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 39b3bcfd3646..24d92156f690 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -549,12 +549,12 @@ export default async function build(dir: string, conf = null): Promise { } if (hasPages404 && page === '/404') { - if (!result.isStatic) { + if (!result.isStatic && !result.hasStaticProps) { throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR) } // we need to ensure the 404 lambda is present since we use // it when _app has getInitialProps - if (customAppGetInitialProps) { + if (customAppGetInitialProps && !result.hasStaticProps) { staticPages.delete(page) } } diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts index ae74148ce825..e8025b25e9ec 100644 --- a/packages/next/lib/constants.ts +++ b/packages/next/lib/constants.ts @@ -30,4 +30,4 @@ export const SERVER_PROPS_GET_INIT_PROPS_CONFLICT = `You can not use getInitialP export const SERVER_PROPS_SSG_CONFLICT = `You can not use getStaticProps with getServerSideProps. To use SSG, please remove getServerSideProps` -export const PAGES_404_GET_INITIAL_PROPS_ERROR = `\`pages/404\` can not have getInitialProps/getServerSideProps/getStaticProps, https://err.sh/zeit/next.js/404-get-initial-props` +export const PAGES_404_GET_INITIAL_PROPS_ERROR = `\`pages/404\` can not have getInitialProps/getServerSideProps, https://err.sh/zeit/next.js/404-get-initial-props` diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 7e5e85d2b636..920b6b70e130 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -418,10 +418,7 @@ export async function renderToHTML( renderOpts.nextExport = true } - if ( - pathname === '/404' && - (hasPageGetInitialProps || getServerSideProps || isSSG) - ) { + if (pathname === '/404' && (hasPageGetInitialProps || getServerSideProps)) { throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR) } } diff --git a/test/integration/404-page-ssg/next.config.js b/test/integration/404-page-ssg/next.config.js new file mode 100644 index 000000000000..4ba52ba2c8df --- /dev/null +++ b/test/integration/404-page-ssg/next.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/test/integration/404-page-ssg/pages/404.js b/test/integration/404-page-ssg/pages/404.js new file mode 100644 index 000000000000..3af534db8fc8 --- /dev/null +++ b/test/integration/404-page-ssg/pages/404.js @@ -0,0 +1,6 @@ +export const getStaticProps = () => ({ + props: { hello: 'world', random: Math.random() }, +}) + +const page = ({ random }) => `custom 404 page ${random}` +export default page diff --git a/test/integration/404-page-ssg/pages/_app.js b/test/integration/404-page-ssg/pages/_app.js new file mode 100644 index 000000000000..93e23041c80e --- /dev/null +++ b/test/integration/404-page-ssg/pages/_app.js @@ -0,0 +1,12 @@ +const App = ({ Component, pageProps }) => + +App.getInitialProps = async ({ Component, ctx }) => { + if (Component.getInitialProps) { + await Component.getInitialProps(ctx) + } + return { + pageProps: {}, + } +} + +export default App diff --git a/test/integration/404-page-ssg/pages/err.js b/test/integration/404-page-ssg/pages/err.js new file mode 100644 index 000000000000..6d0f2c17817a --- /dev/null +++ b/test/integration/404-page-ssg/pages/err.js @@ -0,0 +1,5 @@ +const page = () => 'err page' +page.getInitialProps = () => { + throw new Error('oops') +} +export default page diff --git a/test/integration/404-page-ssg/pages/index.js b/test/integration/404-page-ssg/pages/index.js new file mode 100644 index 000000000000..f6c15d1f66e8 --- /dev/null +++ b/test/integration/404-page-ssg/pages/index.js @@ -0,0 +1 @@ +export default () => 'hello from index' diff --git a/test/integration/404-page-ssg/test/index.test.js b/test/integration/404-page-ssg/test/index.test.js new file mode 100644 index 000000000000..487ecae21109 --- /dev/null +++ b/test/integration/404-page-ssg/test/index.test.js @@ -0,0 +1,180 @@ +/* eslint-env jest */ +/* global jasmine */ +import fs from 'fs-extra' +import { join } from 'path' +import { + killApp, + findPort, + launchApp, + nextStart, + nextBuild, + renderViaHTTP, + fetchViaHTTP, +} from 'next-test-utils' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 + +const appDir = join(__dirname, '../') +const nextConfig = join(appDir, 'next.config.js') +const gip404Err = /`pages\/404` can not have getInitialProps\/getServerSideProps/ + +let nextConfigContent +let stdout +let stderr +let buildId +let appPort +let app + +const runTests = isDev => { + it('should respond to 404 correctly', async () => { + const res = await fetchViaHTTP(appPort, '/404') + expect(res.status).toBe(404) + expect(await res.text()).toContain('custom 404 page') + }) + + it('should render error correctly', async () => { + const text = await renderViaHTTP(appPort, '/err') + expect(text).toContain(isDev ? 'oops' : 'An unexpected error has occurred') + }) + + it('should not show an error in the logs for 404 SSG', async () => { + await renderViaHTTP(appPort, '/non-existent') + expect(stderr).not.toMatch(gip404Err) + expect(stdout).not.toMatch(gip404Err) + }) + + it('should render index page normal', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html).toContain('hello from index') + }) + + if (!isDev) { + it('should not revalidate custom 404 page', async () => { + const res1 = await renderViaHTTP(appPort, '/non-existent') + const res2 = await renderViaHTTP(appPort, '/non-existent') + const res3 = await renderViaHTTP(appPort, '/non-existent') + const res4 = await renderViaHTTP(appPort, '/non-existent') + + expect(res1 === res2 && res2 === res3 && res3 === res4).toBe(true) + + expect(res1).toContain('custom 404 page') + }) + + it('should set pages404 in routes-manifest correctly', async () => { + const data = await fs.readJSON(join(appDir, '.next/routes-manifest.json')) + expect(data.pages404).toBe(true) + }) + + it('should have 404 page in prerender-manifest', async () => { + const data = await fs.readJSON( + join(appDir, '.next/prerender-manifest.json') + ) + expect(data.routes['/404']).toEqual({ + initialRevalidateSeconds: false, + srcRoute: null, + dataRoute: `/_next/data/${buildId}/404.json`, + }) + }) + } +} + +describe('404 Page Support SSG', () => { + describe('server mode', () => { + afterAll(() => killApp(app)) + + it('should build successfully', async () => { + nextConfigContent = await fs.readFile(nextConfig, 'utf8') + const { + code, + stderr: buildStderr, + stdout: buildStdout, + } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + expect(buildStderr).not.toMatch(gip404Err) + expect(buildStdout).not.toMatch(gip404Err) + + appPort = await findPort() + stderr = '' + stdout = '' + + app = await nextStart(appDir, appPort, { + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + }) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests() + }) + + describe('serverless mode', () => { + afterAll(async () => { + await fs.writeFile(nextConfig, nextConfigContent) + await killApp(app) + }) + + it('should build successfully', async () => { + nextConfigContent = await fs.readFile(nextConfig, 'utf8') + await fs.writeFile( + nextConfig, + ` + module.exports = { target: 'experimental-serverless-trace' } + ` + ) + const { + code, + stderr: buildStderr, + stdout: buildStdout, + } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + expect(buildStderr).not.toMatch(gip404Err) + expect(buildStdout).not.toMatch(gip404Err) + + appPort = await findPort() + stderr = '' + stdout = '' + app = await nextStart(appDir, appPort, { + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + }) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests() + }) + + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + stderr = '' + stdout = '' + app = await launchApp(appDir, appPort, { + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + }) + }) + afterAll(() => killApp(app)) + + runTests(true) + }) +}) diff --git a/test/integration/404-page/test/index.test.js b/test/integration/404-page/test/index.test.js index 6c481628d24a..913304ede143 100644 --- a/test/integration/404-page/test/index.test.js +++ b/test/integration/404-page/test/index.test.js @@ -182,7 +182,7 @@ describe('404 Page Support', () => { expect(stderr).toMatch(gip404Err) }) - it('shows error with getStaticProps in pages/404 build', async () => { + it('does not show error with getStaticProps in pages/404 build', async () => { await fs.move(pages404, `${pages404}.bak`) await fs.writeFile( pages404, @@ -196,11 +196,11 @@ describe('404 Page Support', () => { await fs.remove(pages404) await fs.move(`${pages404}.bak`, pages404) - expect(stderr).toMatch(gip404Err) - expect(code).toBe(1) + expect(stderr).not.toMatch(gip404Err) + expect(code).toBe(0) }) - it('shows error with getStaticProps in pages/404 dev', async () => { + it('does not show error with getStaticProps in pages/404 dev', async () => { await fs.move(pages404, `${pages404}.bak`) await fs.writeFile( pages404, @@ -226,7 +226,7 @@ describe('404 Page Support', () => { await fs.remove(pages404) await fs.move(`${pages404}.bak`, pages404) - expect(stderr).toMatch(gip404Err) + expect(stderr).not.toMatch(gip404Err) }) it('shows error with getServerSideProps in pages/404 build', async () => {