Skip to content

Commit 4782bda

Browse files
authored
Add support for notFound in getServerSideProps (#18241)
This is a follow-up to #17755 which adds support for returning `notFound` to `getServerSideProps` also
1 parent 3f22490 commit 4782bda

File tree

8 files changed

+169
-15
lines changed

8 files changed

+169
-15
lines changed

packages/next/build/webpack/loaders/next-serverless-loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ const nextServerlessLoader: loader.Loader = function () {
772772
773773
if (!renderMode) {
774774
if (_nextData || getStaticProps || getServerSideProps) {
775-
if (renderOpts.ssgNotFound) {
775+
if (renderOpts.isNotFound) {
776776
res.statusCode = 404
777777
778778
const NotFoundComponent = ${

packages/next/export/worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ export default async function exportPage({
263263
html = (result as any).html
264264
}
265265

266-
if (!html && !(curRenderOpts as any).ssgNotFound) {
266+
if (!html && !(curRenderOpts as any).isNotFound) {
267267
throw new Error(`Failed to render serverless page`)
268268
}
269269
} else {
@@ -318,7 +318,7 @@ export default async function exportPage({
318318
html = await renderMethod(req, res, page, query, curRenderOpts)
319319
}
320320
}
321-
results.ssgNotFound = (curRenderOpts as any).ssgNotFound
321+
results.ssgNotFound = (curRenderOpts as any).isNotFound
322322

323323
const validateAmp = async (
324324
rawAmpHtml: string,

packages/next/next-server/server/next-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,7 +1351,7 @@ export default class Server {
13511351
html = renderResult.html
13521352
pageData = renderResult.renderOpts.pageData
13531353
sprRevalidate = renderResult.renderOpts.revalidate
1354-
isNotFound = renderResult.renderOpts.ssgNotFound
1354+
isNotFound = renderResult.renderOpts.isNotFound
13551355
} else {
13561356
const origQuery = parseUrl(req.url || '', true).query
13571357
const resolvedUrl = formatUrl({
@@ -1393,7 +1393,7 @@ export default class Server {
13931393
// TODO: change this to a different passing mechanism
13941394
pageData = (renderOpts as any).pageData
13951395
sprRevalidate = (renderOpts as any).revalidate
1396-
isNotFound = (renderOpts as any).ssgNotFound
1396+
isNotFound = (renderOpts as any).isNotFound
13971397
}
13981398

13991399
return { html, pageData, sprRevalidate, isNotFound }

packages/next/next-server/server/render.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ export async function renderToHTML(
643643
)
644644
}
645645

646-
;(renderOpts as any).ssgNotFound = true
646+
;(renderOpts as any).isNotFound = true
647647
;(renderOpts as any).revalidate = false
648648
return null
649649
}
@@ -753,21 +753,35 @@ export async function renderToHTML(
753753
}
754754

755755
const invalidKeys = Object.keys(data).filter(
756-
(key) => key !== 'props' && key !== 'unstable_redirect'
756+
(key) =>
757+
key !== 'props' &&
758+
key !== 'unstable_redirect' &&
759+
key !== 'unstable_notFound'
757760
)
758761

759762
if (invalidKeys.length) {
760763
throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys))
761764
}
762765

766+
if ('unstable_notFound' in data) {
767+
if (pathname === '/404') {
768+
throw new Error(
769+
`The /404 page can not return unstable_notFound in "getStaticProps", please remove it to continue!`
770+
)
771+
}
772+
773+
;(renderOpts as any).isNotFound = true
774+
return null
775+
}
776+
763777
if (
764-
data.unstable_redirect &&
778+
'unstable_redirect' in data &&
765779
typeof data.unstable_redirect === 'object'
766780
) {
767781
checkRedirectValues(data.unstable_redirect, req)
768782

769783
if (isDataReq) {
770-
data.props = {
784+
;(data as any).props = {
771785
__N_REDIRECT: data.unstable_redirect.destination,
772786
}
773787
} else {
@@ -778,15 +792,19 @@ export async function renderToHTML(
778792

779793
if (
780794
(dev || isBuildTimeSSG) &&
781-
!isSerializableProps(pathname, 'getServerSideProps', data.props)
795+
!isSerializableProps(
796+
pathname,
797+
'getServerSideProps',
798+
(data as any).props
799+
)
782800
) {
783801
// this fn should throw an error instead of ever returning `false`
784802
throw new Error(
785803
'invariant: getServerSideProps did not return valid props. Please report this.'
786804
)
787805
}
788806

789-
props.pageProps = Object.assign({}, props.pageProps, data.props)
807+
props.pageProps = Object.assign({}, props.pageProps, (data as any).props)
790808
;(renderOpts as any).pageData = props
791809
}
792810
} catch (dataFetchError) {

packages/next/types/index.d.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,16 @@ export type GetServerSidePropsContext<
132132
locales?: string[]
133133
}
134134

135-
export type GetServerSidePropsResult<P> = {
136-
props?: P
137-
unstable_redirect?: Redirect
138-
}
135+
export type GetServerSidePropsResult<P> =
136+
| {
137+
props: P
138+
}
139+
| {
140+
unstable_redirect: Redirect
141+
}
142+
| {
143+
unstable_notFound: true
144+
}
139145

140146
export type GetServerSideProps<
141147
P extends { [key: string]: any } = { [key: string]: any },
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Link from 'next/link'
2+
import { useRouter } from 'next/router'
3+
4+
export default function Page(props) {
5+
const router = useRouter()
6+
7+
return (
8+
<>
9+
<p id="gssp">gssp page</p>
10+
<p id="props">{JSON.stringify(props)}</p>
11+
<p id="router-query">{JSON.stringify(router.query)}</p>
12+
<p id="router-pathname">{router.pathname}</p>
13+
<p id="router-as-path">{router.asPath}</p>
14+
<Link href="/">
15+
<a id="to-index">to /</a>
16+
</Link>
17+
<br />
18+
</>
19+
)
20+
}
21+
22+
export const getServerSideProps = ({ query }) => {
23+
if (query.hiding) {
24+
return {
25+
unstable_notFound: true,
26+
}
27+
}
28+
29+
return {
30+
props: {
31+
hello: 'world',
32+
},
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Link from 'next/link'
2+
import { useRouter } from 'next/router'
3+
4+
export default function Page(props) {
5+
const router = useRouter()
6+
7+
return (
8+
<>
9+
<p id="gssp">gssp page</p>
10+
<p id="props">{JSON.stringify(props)}</p>
11+
<p id="router-query">{JSON.stringify(router.query)}</p>
12+
<p id="router-pathname">{router.pathname}</p>
13+
<p id="router-as-path">{router.asPath}</p>
14+
<Link href="/">
15+
<a id="to-index">to /</a>
16+
</Link>
17+
<br />
18+
</>
19+
)
20+
}
21+
22+
export const getServerSideProps = ({ query }) => {
23+
if (query.hiding) {
24+
return {
25+
unstable_notFound: true,
26+
}
27+
}
28+
29+
return {
30+
props: {
31+
hello: 'world',
32+
},
33+
}
34+
}

test/integration/getserversideprops/test/index.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ const expectedManifestRoutes = () => [
118118
),
119119
page: '/non-json',
120120
},
121+
{
122+
dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
123+
buildId
124+
)}\\/not-found.json$`,
125+
page: '/not-found',
126+
},
127+
{
128+
dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
129+
buildId
130+
)}\\/not\\-found\\/([^\\/]+?)\\.json$`,
131+
namedDataRouteRegex: `^/_next/data/${escapeRegex(
132+
buildId
133+
)}/not\\-found/(?<slug>[^/]+?)\\.json$`,
134+
page: '/not-found/[slug]',
135+
routeKeys: {
136+
slug: 'slug',
137+
},
138+
},
121139
{
122140
dataRouteRegex: normalizeRegEx(
123141
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/refresh.json$`
@@ -235,6 +253,50 @@ const navigateTest = (dev = false) => {
235253
const runTests = (dev = false) => {
236254
navigateTest(dev)
237255

256+
it('should render 404 correctly when notFound is returned (non-dynamic)', async () => {
257+
const res = await fetchViaHTTP(appPort, '/not-found', { hiding: true })
258+
259+
expect(res.status).toBe(404)
260+
expect(await res.text()).toContain('This page could not be found')
261+
})
262+
263+
it('should render 404 correctly when notFound is returned client-transition (non-dynamic)', async () => {
264+
const browser = await webdriver(appPort, '/')
265+
await browser.eval(`(function() {
266+
window.beforeNav = 1
267+
window.next.router.push('/not-found?hiding=true')
268+
})()`)
269+
270+
await browser.waitForElementByCss('h1')
271+
expect(await browser.elementByCss('html').text()).toContain(
272+
'This page could not be found'
273+
)
274+
expect(await browser.eval('window.beforeNav')).toBe(null)
275+
})
276+
277+
it('should render 404 correctly when notFound is returned (dynamic)', async () => {
278+
const res = await fetchViaHTTP(appPort, '/not-found/first', {
279+
hiding: true,
280+
})
281+
282+
expect(res.status).toBe(404)
283+
expect(await res.text()).toContain('This page could not be found')
284+
})
285+
286+
it('should render 404 correctly when notFound is returned client-transition (dynamic)', async () => {
287+
const browser = await webdriver(appPort, '/')
288+
await browser.eval(`(function() {
289+
window.beforeNav = 1
290+
window.next.router.push('/not-found/first?hiding=true')
291+
})()`)
292+
293+
await browser.waitForElementByCss('h1')
294+
expect(await browser.elementByCss('html').text()).toContain(
295+
'This page could not be found'
296+
)
297+
expect(await browser.eval('window.beforeNav')).toBe(null)
298+
})
299+
238300
it('should SSR normal page correctly', async () => {
239301
const html = await renderViaHTTP(appPort, '/')
240302
expect(html).toMatch(/hello.*?world/)

0 commit comments

Comments
 (0)