Skip to content

Commit bc80fb4

Browse files
ijjktimneutkens
andauthored
Add handling for redirects from getStaticProps/getServerSideProps (vercel#16642)
Co-authored-by: Tim Neutkens <[email protected]>
1 parent 6935a93 commit bc80fb4

File tree

13 files changed

+517
-9
lines changed

13 files changed

+517
-9
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Redirect During getStaticProps Prerendering
2+
3+
#### Why This Error Occurred
4+
5+
The `redirect` value was returned from `getStaticProps` during prerendering which is invalid.
6+
7+
#### Possible Ways to Fix It
8+
9+
Remove any paths that result in a redirect from being prerendered in `getStaticPaths` and enable `fallback: true` to handle redirecting for these pages.
10+
11+
### Useful Links
12+
13+
- [Data Fetching Documentation](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)

errors/invalid-redirect-gssp.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Invalid Redirect getStaticProps/getServerSideProps
2+
3+
#### Why This Error Occurred
4+
5+
The `redirect` value returned from your `getStaticProps` or `getServerSideProps` function had invalid values.
6+
7+
#### Possible Ways to Fix It
8+
9+
Make sure you return the proper values for the `redirect` value.
10+
11+
```js
12+
export const getStaticProps = ({ params }) => {
13+
if (params.slug === 'deleted-post') {
14+
return {
15+
redirect: {
16+
permanent: true // or false
17+
destination: '/some-location'
18+
}
19+
}
20+
}
21+
22+
return {
23+
props: {
24+
// data
25+
}
26+
}
27+
}
28+
```
29+
30+
### Useful Links
31+
32+
- [Data Fetching Documentation](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)

packages/next/next-server/lib/router/router.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,37 @@ export default class Router implements BaseRouter {
678678
as,
679679
shallow
680680
)
681-
let { error } = routeInfo
681+
let { error, props, __N_SSG, __N_SSP } = routeInfo
682+
683+
// handle redirect on client-transition
684+
if (
685+
(__N_SSG || __N_SSP) &&
686+
props &&
687+
(props as any).pageProps &&
688+
(props as any).pageProps.__N_REDIRECT
689+
) {
690+
const destination = (props as any).pageProps.__N_REDIRECT
691+
692+
// check if destination is internal (resolves to a page) and attempt
693+
// client-navigation if it is falling back to hard navigation if
694+
// it's not
695+
if (destination.startsWith('/')) {
696+
const parsedHref = parseRelativeUrl(destination)
697+
this._resolveHref(parsedHref, pages)
698+
699+
if (pages.includes(parsedHref.pathname)) {
700+
return this.change(
701+
'replaceState',
702+
destination,
703+
destination,
704+
options
705+
)
706+
}
707+
}
708+
709+
window.location.href = destination
710+
return new Promise(() => {})
711+
}
682712

683713
Router.events.emit('beforeHistoryChange', as)
684714
this.changeState(method, url, as, options)
@@ -869,6 +899,7 @@ export default class Router implements BaseRouter {
869899
} as any
870900
)
871901
)
902+
872903
routeInfo.props = props
873904
this.components[route] = routeInfo
874905
return routeInfo

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

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
AMP_RENDER_TARGET,
2222
SERVER_PROPS_ID,
2323
STATIC_PROPS_ID,
24+
PERMANENT_REDIRECT_STATUS,
25+
TEMPORARY_REDIRECT_STATUS,
2426
} from '../lib/constants'
2527
import { defaultHead } from '../lib/head'
2628
import { HeadManagerContext } from '../lib/head-manager-context'
@@ -264,6 +266,46 @@ const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => {
264266
)
265267
}
266268

269+
type Redirect = {
270+
permanent: boolean
271+
destination: string
272+
}
273+
274+
function checkRedirectValues(redirect: Redirect, req: IncomingMessage) {
275+
const { destination, permanent } = redirect
276+
let invalidPermanent = typeof permanent !== 'boolean'
277+
let invalidDestination = typeof destination !== 'string'
278+
279+
if (invalidPermanent || invalidDestination) {
280+
throw new Error(
281+
`Invalid redirect object returned from getStaticProps for ${req.url}\n` +
282+
`Expected${
283+
invalidPermanent
284+
? ` \`permanent\` to be boolean but received ${typeof permanent}`
285+
: ''
286+
}${invalidPermanent && invalidDestination ? ' and' : ''}${
287+
invalidDestination
288+
? ` \`destinatino\` to be string but received ${typeof destination}`
289+
: ''
290+
}\n` +
291+
`See more info here: https://err.sh/vercel/next.js/invalid-redirect-gssp`
292+
)
293+
}
294+
}
295+
296+
function handleRedirect(res: ServerResponse, redirect: Redirect) {
297+
const statusCode = redirect.permanent
298+
? PERMANENT_REDIRECT_STATUS
299+
: TEMPORARY_REDIRECT_STATUS
300+
301+
if (redirect.permanent) {
302+
res.setHeader('Refresh', `0;url=${redirect.destination}`)
303+
}
304+
res.statusCode = statusCode
305+
res.setHeader('Location', redirect.destination)
306+
res.end()
307+
}
308+
267309
export async function renderToHTML(
268310
req: IncomingMessage,
269311
res: ServerResponse,
@@ -534,7 +576,8 @@ export async function renderToHTML(
534576
}
535577

536578
const invalidKeys = Object.keys(data).filter(
537-
(key) => key !== 'revalidate' && key !== 'props'
579+
(key) =>
580+
key !== 'revalidate' && key !== 'props' && key !== 'unstable_redirect'
538581
)
539582

540583
if (invalidKeys.includes('unstable_revalidate')) {
@@ -545,6 +588,29 @@ export async function renderToHTML(
545588
throw new Error(invalidKeysMsg('getStaticProps', invalidKeys))
546589
}
547590

591+
if (
592+
data.unstable_redirect &&
593+
typeof data.unstable_redirect === 'object'
594+
) {
595+
checkRedirectValues(data.unstable_redirect, req)
596+
597+
if (isBuildTimeSSG) {
598+
throw new Error(
599+
`\`redirect\` can not be returned from getStaticProps during prerendering (${req.url})\n` +
600+
`See more info here: https://err.sh/next.js/gsp-redirect-during-prerender`
601+
)
602+
}
603+
604+
if (isDataReq) {
605+
data.props = {
606+
__N_REDIRECT: data.unstable_redirect.destination,
607+
}
608+
} else {
609+
handleRedirect(res, data.unstable_redirect)
610+
return null
611+
}
612+
}
613+
548614
if (
549615
(dev || isBuildTimeSSG) &&
550616
!isSerializableProps(pathname, 'getStaticProps', data.props)
@@ -623,12 +689,30 @@ export async function renderToHTML(
623689
throw new Error(GSSP_NO_RETURNED_VALUE)
624690
}
625691

626-
const invalidKeys = Object.keys(data).filter((key) => key !== 'props')
692+
const invalidKeys = Object.keys(data).filter(
693+
(key) => key !== 'props' && key !== 'unstable_redirect'
694+
)
627695

628696
if (invalidKeys.length) {
629697
throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys))
630698
}
631699

700+
if (
701+
data.unstable_redirect &&
702+
typeof data.unstable_redirect === 'object'
703+
) {
704+
checkRedirectValues(data.unstable_redirect, req)
705+
706+
if (isDataReq) {
707+
data.props = {
708+
__N_REDIRECT: data.unstable_redirect.destination,
709+
}
710+
} else {
711+
handleRedirect(res, data.unstable_redirect)
712+
return null
713+
}
714+
}
715+
632716
if (
633717
(dev || isBuildTimeSSG) &&
634718
!isSerializableProps(pathname, 'getServerSideProps', data.props)

packages/next/types/index.d.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,21 @@ export {
7272
NextApiHandler,
7373
}
7474

75+
type Redirect = {
76+
permanent: boolean
77+
destination: string
78+
}
79+
7580
export type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
7681
params?: Q
7782
preview?: boolean
7883
previewData?: any
7984
}
8085

8186
export type GetStaticPropsResult<P> = {
82-
props: P
87+
props?: P
8388
revalidate?: number | boolean
89+
unstable_redirect?: Redirect
8490
}
8591

8692
export type GetStaticProps<
@@ -117,7 +123,8 @@ export type GetServerSidePropsContext<
117123
}
118124

119125
export type GetServerSidePropsResult<P> = {
120-
props: P
126+
props?: P
127+
unstable_redirect?: Redirect
121128
}
122129

123130
export type GetServerSideProps<

test/integration/build-output/test/index.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,16 @@ describe('Build Output', () => {
9595
expect(indexSize.endsWith('B')).toBe(true)
9696

9797
// should be no bigger than 60.2 kb
98-
expect(parseFloat(indexFirstLoad) - 60.3).toBeLessThanOrEqual(0)
98+
expect(parseFloat(indexFirstLoad) - 60.4).toBeLessThanOrEqual(0)
9999
expect(indexFirstLoad.endsWith('kB')).toBe(true)
100100

101101
expect(parseFloat(err404Size) - 3.5).toBeLessThanOrEqual(0)
102102
expect(err404Size.endsWith('kB')).toBe(true)
103103

104-
expect(parseFloat(err404FirstLoad) - 63.4).toBeLessThanOrEqual(0)
104+
expect(parseFloat(err404FirstLoad) - 63.6).toBeLessThanOrEqual(0)
105105
expect(err404FirstLoad.endsWith('kB')).toBe(true)
106106

107-
expect(parseFloat(sharedByAll) - 60).toBeLessThanOrEqual(0)
107+
expect(parseFloat(sharedByAll) - 60.1).toBeLessThanOrEqual(0)
108108
expect(sharedByAll.endsWith('kB')).toBe(true)
109109

110110
if (_appSize.endsWith('kB')) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function NotFound() {
2+
return <p>oops not found</p>
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Another() {
2+
return <p id="another">another Page</p>
3+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useRouter } from 'next/router'
2+
3+
export default function Post(props) {
4+
const router = useRouter()
5+
6+
if (typeof window !== 'undefined' && !window.initialHref) {
7+
window.initialHref = window.location.href
8+
}
9+
10+
if (router.isFallback) return <p>Loading...</p>
11+
12+
return (
13+
<>
14+
<p id="gsp">getStaticProps</p>
15+
<p id="props">{JSON.stringify(props)}</p>
16+
</>
17+
)
18+
}
19+
20+
export const getStaticProps = ({ params }) => {
21+
if (params.post.startsWith('redir')) {
22+
let destination = '/404'
23+
24+
if (params.post.includes('dest-')) {
25+
destination = params.post.split('dest-').pop().replace(/_/g, '/')
26+
}
27+
28+
return {
29+
unstable_redirect: {
30+
destination,
31+
permanent: params.post.includes('permanent'),
32+
},
33+
}
34+
}
35+
36+
return {
37+
props: {
38+
params,
39+
},
40+
}
41+
}
42+
43+
export const getStaticPaths = () => {
44+
return {
45+
paths: ['first', 'second'].map((post) => ({ params: { post } })),
46+
fallback: true,
47+
}
48+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export default function Post(props) {
2+
return (
3+
<>
4+
<p id="gssp">getServerSideProps</p>
5+
<p id="props">{JSON.stringify(props)}</p>
6+
</>
7+
)
8+
}
9+
10+
export const getServerSideProps = ({ params }) => {
11+
if (params.post.startsWith('redir')) {
12+
let destination = '/404'
13+
14+
if (params.post.includes('dest-')) {
15+
destination = params.post.split('dest-').pop().replace(/_/g, '/')
16+
}
17+
18+
return {
19+
unstable_redirect: {
20+
destination,
21+
permanent: params.post.includes('permanent'),
22+
},
23+
}
24+
}
25+
26+
return {
27+
props: {
28+
params,
29+
},
30+
}
31+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Index() {
2+
return <p id="index">Index Page</p>
3+
}

0 commit comments

Comments
 (0)