Skip to content

Commit

Permalink
Add handling for redirects from getStaticProps/getServerSideProps (ve…
Browse files Browse the repository at this point in the history
…rcel#16642)

Co-authored-by: Tim Neutkens <timneutkens@me.com>
  • Loading branch information
2 people authored and Piotr Bosak committed Sep 26, 2020
1 parent 33a9213 commit 3c839f3
Show file tree
Hide file tree
Showing 13 changed files with 517 additions and 9 deletions.
13 changes: 13 additions & 0 deletions errors/gsp-redirect-during-prerender.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Redirect During getStaticProps Prerendering

#### Why This Error Occurred

The `redirect` value was returned from `getStaticProps` during prerendering which is invalid.

#### Possible Ways to Fix It

Remove any paths that result in a redirect from being prerendered in `getStaticPaths` and enable `fallback: true` to handle redirecting for these pages.

### Useful Links

- [Data Fetching Documentation](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)
32 changes: 32 additions & 0 deletions errors/invalid-redirect-gssp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Invalid Redirect getStaticProps/getServerSideProps

#### Why This Error Occurred

The `redirect` value returned from your `getStaticProps` or `getServerSideProps` function had invalid values.

#### Possible Ways to Fix It

Make sure you return the proper values for the `redirect` value.

```js
export const getStaticProps = ({ params }) => {
if (params.slug === 'deleted-post') {
return {
redirect: {
permanent: true // or false
destination: '/some-location'
}
}
}

return {
props: {
// data
}
}
}
```

### Useful Links

- [Data Fetching Documentation](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)
33 changes: 32 additions & 1 deletion packages/next/next-server/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,37 @@ export default class Router implements BaseRouter {
as,
shallow
)
let { error } = routeInfo
let { error, props, __N_SSG, __N_SSP } = routeInfo

// handle redirect on client-transition
if (
(__N_SSG || __N_SSP) &&
props &&
(props as any).pageProps &&
(props as any).pageProps.__N_REDIRECT
) {
const destination = (props as any).pageProps.__N_REDIRECT

// check if destination is internal (resolves to a page) and attempt
// client-navigation if it is falling back to hard navigation if
// it's not
if (destination.startsWith('/')) {
const parsedHref = parseRelativeUrl(destination)
this._resolveHref(parsedHref, pages)

if (pages.includes(parsedHref.pathname)) {
return this.change(
'replaceState',
destination,
destination,
options
)
}
}

window.location.href = destination
return new Promise(() => {})
}

Router.events.emit('beforeHistoryChange', as)
this.changeState(method, url, as, options)
Expand Down Expand Up @@ -869,6 +899,7 @@ export default class Router implements BaseRouter {
} as any
)
)

routeInfo.props = props
this.components[route] = routeInfo
return routeInfo
Expand Down
88 changes: 86 additions & 2 deletions packages/next/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
AMP_RENDER_TARGET,
SERVER_PROPS_ID,
STATIC_PROPS_ID,
PERMANENT_REDIRECT_STATUS,
TEMPORARY_REDIRECT_STATUS,
} from '../lib/constants'
import { defaultHead } from '../lib/head'
import { HeadManagerContext } from '../lib/head-manager-context'
Expand Down Expand Up @@ -264,6 +266,46 @@ const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => {
)
}

type Redirect = {
permanent: boolean
destination: string
}

function checkRedirectValues(redirect: Redirect, req: IncomingMessage) {
const { destination, permanent } = redirect
let invalidPermanent = typeof permanent !== 'boolean'
let invalidDestination = typeof destination !== 'string'

if (invalidPermanent || invalidDestination) {
throw new Error(
`Invalid redirect object returned from getStaticProps for ${req.url}\n` +
`Expected${
invalidPermanent
? ` \`permanent\` to be boolean but received ${typeof permanent}`
: ''
}${invalidPermanent && invalidDestination ? ' and' : ''}${
invalidDestination
? ` \`destinatino\` to be string but received ${typeof destination}`
: ''
}\n` +
`See more info here: https://err.sh/vercel/next.js/invalid-redirect-gssp`
)
}
}

function handleRedirect(res: ServerResponse, redirect: Redirect) {
const statusCode = redirect.permanent
? PERMANENT_REDIRECT_STATUS
: TEMPORARY_REDIRECT_STATUS

if (redirect.permanent) {
res.setHeader('Refresh', `0;url=${redirect.destination}`)
}
res.statusCode = statusCode
res.setHeader('Location', redirect.destination)
res.end()
}

export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -534,7 +576,8 @@ export async function renderToHTML(
}

const invalidKeys = Object.keys(data).filter(
(key) => key !== 'revalidate' && key !== 'props'
(key) =>
key !== 'revalidate' && key !== 'props' && key !== 'unstable_redirect'
)

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

if (
data.unstable_redirect &&
typeof data.unstable_redirect === 'object'
) {
checkRedirectValues(data.unstable_redirect, req)

if (isBuildTimeSSG) {
throw new Error(
`\`redirect\` can not be returned from getStaticProps during prerendering (${req.url})\n` +
`See more info here: https://err.sh/next.js/gsp-redirect-during-prerender`
)
}

if (isDataReq) {
data.props = {
__N_REDIRECT: data.unstable_redirect.destination,
}
} else {
handleRedirect(res, data.unstable_redirect)
return null
}
}

if (
(dev || isBuildTimeSSG) &&
!isSerializableProps(pathname, 'getStaticProps', data.props)
Expand Down Expand Up @@ -623,12 +689,30 @@ export async function renderToHTML(
throw new Error(GSSP_NO_RETURNED_VALUE)
}

const invalidKeys = Object.keys(data).filter((key) => key !== 'props')
const invalidKeys = Object.keys(data).filter(
(key) => key !== 'props' && key !== 'unstable_redirect'
)

if (invalidKeys.length) {
throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys))
}

if (
data.unstable_redirect &&
typeof data.unstable_redirect === 'object'
) {
checkRedirectValues(data.unstable_redirect, req)

if (isDataReq) {
data.props = {
__N_REDIRECT: data.unstable_redirect.destination,
}
} else {
handleRedirect(res, data.unstable_redirect)
return null
}
}

if (
(dev || isBuildTimeSSG) &&
!isSerializableProps(pathname, 'getServerSideProps', data.props)
Expand Down
11 changes: 9 additions & 2 deletions packages/next/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,21 @@ export {
NextApiHandler,
}

type Redirect = {
permanent: boolean
destination: string
}

export type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
params?: Q
preview?: boolean
previewData?: any
}

export type GetStaticPropsResult<P> = {
props: P
props?: P
revalidate?: number | boolean
unstable_redirect?: Redirect
}

export type GetStaticProps<
Expand Down Expand Up @@ -117,7 +123,8 @@ export type GetServerSidePropsContext<
}

export type GetServerSidePropsResult<P> = {
props: P
props?: P
unstable_redirect?: Redirect
}

export type GetServerSideProps<
Expand Down
6 changes: 3 additions & 3 deletions test/integration/build-output/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,16 @@ describe('Build Output', () => {
expect(indexSize.endsWith('B')).toBe(true)

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

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

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

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

if (_appSize.endsWith('kB')) {
Expand Down
3 changes: 3 additions & 0 deletions test/integration/gssp-redirect/pages/404.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function NotFound() {
return <p>oops not found</p>
}
3 changes: 3 additions & 0 deletions test/integration/gssp-redirect/pages/another.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Another() {
return <p id="another">another Page</p>
}
48 changes: 48 additions & 0 deletions test/integration/gssp-redirect/pages/gsp-blog/[post].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useRouter } from 'next/router'

export default function Post(props) {
const router = useRouter()

if (typeof window !== 'undefined' && !window.initialHref) {
window.initialHref = window.location.href
}

if (router.isFallback) return <p>Loading...</p>

return (
<>
<p id="gsp">getStaticProps</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}

export const getStaticProps = ({ params }) => {
if (params.post.startsWith('redir')) {
let destination = '/404'

if (params.post.includes('dest-')) {
destination = params.post.split('dest-').pop().replace(/_/g, '/')
}

return {
unstable_redirect: {
destination,
permanent: params.post.includes('permanent'),
},
}
}

return {
props: {
params,
},
}
}

export const getStaticPaths = () => {
return {
paths: ['first', 'second'].map((post) => ({ params: { post } })),
fallback: true,
}
}
31 changes: 31 additions & 0 deletions test/integration/gssp-redirect/pages/gssp-blog/[post].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export default function Post(props) {
return (
<>
<p id="gssp">getServerSideProps</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}

export const getServerSideProps = ({ params }) => {
if (params.post.startsWith('redir')) {
let destination = '/404'

if (params.post.includes('dest-')) {
destination = params.post.split('dest-').pop().replace(/_/g, '/')
}

return {
unstable_redirect: {
destination,
permanent: params.post.includes('permanent'),
},
}
}

return {
props: {
params,
},
}
}
3 changes: 3 additions & 0 deletions test/integration/gssp-redirect/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Index() {
return <p id="index">Index Page</p>
}
Loading

0 comments on commit 3c839f3

Please sign in to comment.