diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 669594fdb2b3b7b..d81de6a9b5debaa 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -772,7 +772,7 @@ const nextServerlessLoader: loader.Loader = function () { if (!renderMode) { if (_nextData || getStaticProps || getServerSideProps) { - if (renderOpts.ssgNotFound) { + if (renderOpts.isNotFound) { res.statusCode = 404 const NotFoundComponent = ${ diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index f13b2c817187f94..0645fc552972f00 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -263,7 +263,7 @@ export default async function exportPage({ html = (result as any).html } - if (!html && !(curRenderOpts as any).ssgNotFound) { + if (!html && !(curRenderOpts as any).isNotFound) { throw new Error(`Failed to render serverless page`) } } else { @@ -318,7 +318,7 @@ export default async function exportPage({ html = await renderMethod(req, res, page, query, curRenderOpts) } } - results.ssgNotFound = (curRenderOpts as any).ssgNotFound + results.ssgNotFound = (curRenderOpts as any).isNotFound const validateAmp = async ( rawAmpHtml: string, diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index e478f1da66bc022..1ecf546d581769d 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -1351,7 +1351,7 @@ export default class Server { html = renderResult.html pageData = renderResult.renderOpts.pageData sprRevalidate = renderResult.renderOpts.revalidate - isNotFound = renderResult.renderOpts.ssgNotFound + isNotFound = renderResult.renderOpts.isNotFound } else { const origQuery = parseUrl(req.url || '', true).query const resolvedUrl = formatUrl({ @@ -1393,7 +1393,7 @@ export default class Server { // TODO: change this to a different passing mechanism pageData = (renderOpts as any).pageData sprRevalidate = (renderOpts as any).revalidate - isNotFound = (renderOpts as any).ssgNotFound + isNotFound = (renderOpts as any).isNotFound } return { html, pageData, sprRevalidate, isNotFound } diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 1cd05e6b21823bf..7aa8cf64faafd0e 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -643,7 +643,7 @@ export async function renderToHTML( ) } - ;(renderOpts as any).ssgNotFound = true + ;(renderOpts as any).isNotFound = true ;(renderOpts as any).revalidate = false return null } @@ -753,21 +753,35 @@ export async function renderToHTML( } const invalidKeys = Object.keys(data).filter( - (key) => key !== 'props' && key !== 'unstable_redirect' + (key) => + key !== 'props' && + key !== 'unstable_redirect' && + key !== 'unstable_notFound' ) if (invalidKeys.length) { throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys)) } + if ('unstable_notFound' in data) { + if (pathname === '/404') { + throw new Error( + `The /404 page can not return unstable_notFound in "getStaticProps", please remove it to continue!` + ) + } + + ;(renderOpts as any).isNotFound = true + return null + } + if ( - data.unstable_redirect && + 'unstable_redirect' in data && typeof data.unstable_redirect === 'object' ) { checkRedirectValues(data.unstable_redirect, req) if (isDataReq) { - data.props = { + ;(data as any).props = { __N_REDIRECT: data.unstable_redirect.destination, } } else { @@ -778,7 +792,11 @@ export async function renderToHTML( if ( (dev || isBuildTimeSSG) && - !isSerializableProps(pathname, 'getServerSideProps', data.props) + !isSerializableProps( + pathname, + 'getServerSideProps', + (data as any).props + ) ) { // this fn should throw an error instead of ever returning `false` throw new Error( @@ -786,7 +804,7 @@ export async function renderToHTML( ) } - props.pageProps = Object.assign({}, props.pageProps, data.props) + props.pageProps = Object.assign({}, props.pageProps, (data as any).props) ;(renderOpts as any).pageData = props } } catch (dataFetchError) { diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 24e8619f1ede4c6..f834d3114fa0efa 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -132,10 +132,16 @@ export type GetServerSidePropsContext< locales?: string[] } -export type GetServerSidePropsResult

= { - props?: P - unstable_redirect?: Redirect -} +export type GetServerSidePropsResult

= + | { + props: P + } + | { + unstable_redirect: Redirect + } + | { + unstable_notFound: true + } export type GetServerSideProps< P extends { [key: string]: any } = { [key: string]: any }, diff --git a/test/integration/getserversideprops/pages/not-found/[slug].js b/test/integration/getserversideprops/pages/not-found/[slug].js new file mode 100644 index 000000000000000..158be98be1ceff0 --- /dev/null +++ b/test/integration/getserversideprops/pages/not-found/[slug].js @@ -0,0 +1,34 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ) +} + +export const getServerSideProps = ({ query }) => { + if (query.hiding) { + return { + unstable_notFound: true, + } + } + + return { + props: { + hello: 'world', + }, + } +} diff --git a/test/integration/getserversideprops/pages/not-found/index.js b/test/integration/getserversideprops/pages/not-found/index.js new file mode 100644 index 000000000000000..158be98be1ceff0 --- /dev/null +++ b/test/integration/getserversideprops/pages/not-found/index.js @@ -0,0 +1,34 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ) +} + +export const getServerSideProps = ({ query }) => { + if (query.hiding) { + return { + unstable_notFound: true, + } + } + + return { + props: { + hello: 'world', + }, + } +} diff --git a/test/integration/getserversideprops/test/index.test.js b/test/integration/getserversideprops/test/index.test.js index 015ec3d7b10b05d..b3e910d30a9fc6b 100644 --- a/test/integration/getserversideprops/test/index.test.js +++ b/test/integration/getserversideprops/test/index.test.js @@ -118,6 +118,24 @@ const expectedManifestRoutes = () => [ ), page: '/non-json', }, + { + dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/not-found.json$`, + page: '/not-found', + }, + { + dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/not\\-found\\/([^\\/]+?)\\.json$`, + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/not\\-found/(?[^/]+?)\\.json$`, + page: '/not-found/[slug]', + routeKeys: { + slug: 'slug', + }, + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/refresh.json$` @@ -235,6 +253,50 @@ const navigateTest = (dev = false) => { const runTests = (dev = false) => { navigateTest(dev) + it('should render 404 correctly when notFound is returned (non-dynamic)', async () => { + const res = await fetchViaHTTP(appPort, '/not-found', { hiding: true }) + + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + it('should render 404 correctly when notFound is returned client-transition (non-dynamic)', async () => { + const browser = await webdriver(appPort, '/') + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/not-found?hiding=true') + })()`) + + await browser.waitForElementByCss('h1') + expect(await browser.elementByCss('html').text()).toContain( + 'This page could not be found' + ) + expect(await browser.eval('window.beforeNav')).toBe(null) + }) + + it('should render 404 correctly when notFound is returned (dynamic)', async () => { + const res = await fetchViaHTTP(appPort, '/not-found/first', { + hiding: true, + }) + + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + it('should render 404 correctly when notFound is returned client-transition (dynamic)', async () => { + const browser = await webdriver(appPort, '/') + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/not-found/first?hiding=true') + })()`) + + await browser.waitForElementByCss('h1') + expect(await browser.elementByCss('html').text()).toContain( + 'This page could not be found' + ) + expect(await browser.eval('window.beforeNav')).toBe(null) + }) + it('should SSR normal page correctly', async () => { const html = await renderViaHTTP(appPort, '/') expect(html).toMatch(/hello.*?world/)