Skip to content
This repository was archived by the owner on May 10, 2021. It is now read-only.

Commit 4f43aef

Browse files
committed
Use multiValueHeaders in Netlify Functions
Netlify recently added support for multiValueHeaders to Netlify Functions. By using multiValueHeaders, we can now support NextJS Preview Mode. Fixes: #10
1 parent ca63b75 commit 4f43aef

File tree

8 files changed

+210
-14
lines changed

8 files changed

+210
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ SSR pages and API endpoints. It is currently not possible to create custom Netli
150150

151151
### Preview Mode
152152

153-
[NextJS Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode) is currently not supported. When you call `res.setPreviewData({})`, NextJS tries to set and send two cookies to the user. But Netlify Functions only support setting one cookie per function call at the moment. This is [a long-standing bug on Netlify's side](https://community.netlify.com/t/multiple-set-cookie-headers-cause-netlify-lambda-to-throw-an-error/975). We're discussing work-arounds. See: [Issue #10](https://github.com/FinnWoelm/next-on-netlify/issues/10)
153+
[NextJS Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode) does not work on pages that are pre-rendered (pages with `getStaticProps`). Netlify currently does not support cookie-based redirects, which are needed for supporting preview mode on pre-rendered pages. Preview mode works correctly on any server-side-rendered pages (pages with `getInitialProps` or `getServerSideProps`). See: [Issue #10](https://github.com/FinnWoelm/next-on-netlify/issues/10)
154154

155155

156156
### Fallbacks for Pages with `getStaticPaths`
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default async function preview(req, res) {
2+
const { query } = req
3+
const { id } = query
4+
5+
// Enable Preview Mode by setting the cookies
6+
res.setPreviewData({})
7+
8+
// Redirect to the path from the fetched post
9+
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
10+
res.writeHead(307, { Location: `/previewTest/${id}` })
11+
res.end()
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default async function exit(_, res) {
2+
// Exit the current user from "Preview Mode". This function accepts no args.
3+
res.clearPreviewData()
4+
5+
// Redirect the user back to the index page.
6+
res.writeHead(307, { Location: '/' })
7+
res.end()
8+
}

cypress/fixtures/pages/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ const Index = ({ shows }) => (
175175
</li>
176176
</ul>
177177

178+
<h1>6. Preview mode? Yes!</h1>
179+
<p>
180+
next-on-netlify supports preview mode.
181+
</p>
182+
183+
<ul>
184+
<li>
185+
<Link href="/previewTest/222">
186+
<a>previewTest/222</a>
187+
</Link>
188+
</li>
189+
</ul>
190+
178191
<h1>6. Static Pages Stay Static</h1>
179192
<p>
180193
next-on-netlify automatically determines which pages are dynamic and
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import Error from 'next/error'
2+
import Link from 'next/link'
3+
4+
const Show = ({ errorCode, show, person }) => {
5+
6+
// If show/person item was not found, render 404 page
7+
if (errorCode) {
8+
return <Error statusCode={errorCode} />
9+
}
10+
11+
// Otherwise, render show
12+
return (
13+
<div>
14+
<p>
15+
This page uses getServerSideProps() and is SSRed.
16+
<br/><br/>
17+
By default, it shows the TV show by ID.
18+
<br/>
19+
But when in preview mode, it shows person by ID instead.
20+
</p>
21+
22+
<hr/>
23+
24+
{ show ? (
25+
<div>
26+
<h1>Show #{show.id}</h1>
27+
<p>
28+
{show.name}
29+
</p>
30+
</div>
31+
) : (
32+
<div>
33+
<h1>Person #{person.id}</h1>
34+
<p>
35+
{person.name}
36+
</p>
37+
</div>
38+
)}
39+
40+
<hr/>
41+
42+
<Link href="/">
43+
<a>Go back home</a>
44+
</Link>
45+
</div>
46+
)
47+
}
48+
49+
export const getServerSideProps = async (context) => {
50+
console.log(context)
51+
const { params, preview } = context
52+
53+
let res = null
54+
let show = null
55+
let person = null
56+
57+
// The ID to render
58+
const { id } = params
59+
60+
// In preview mode, load person by ID
61+
if(preview) {
62+
res = await fetch(`https://api.tvmaze.com/people/${id}`);
63+
person = await res.json();
64+
}
65+
// In normal mode, load TV show by ID
66+
else {
67+
res = await fetch(`https://api.tvmaze.com/shows/${id}`);
68+
show = await res.json();
69+
}
70+
71+
// Set error code if show/person could not be found
72+
const errorCode = res.status > 200 ? res.status : false
73+
74+
return {
75+
props: {
76+
errorCode,
77+
show,
78+
person
79+
}
80+
}
81+
}
82+
83+
export default Show

cypress/integration/default_spec.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,62 @@ describe('API endpoint', () => {
456456
})
457457
})
458458

459+
describe('Preview Mode', () => {
460+
it('redirects to preview test page', () => {
461+
cy.visit('/api/enterPreview?id=999')
462+
463+
cy.url().should('include', '/previewTest/999')
464+
})
465+
466+
it('sets cookies on client', () => {
467+
Cypress.Cookies.debug(true)
468+
cy.getCookie('__prerender_bypass').should('not.exist')
469+
cy.getCookie('__next_preview_data').should('not.exist')
470+
471+
cy.visit('/api/enterPreview?id=999')
472+
473+
cy.getCookie('__prerender_bypass').should('not.be', null)
474+
cy.getCookie('__next_preview_data').should('not.be', null)
475+
})
476+
477+
it('renders page in preview mode', () => {
478+
cy.visit('/api/enterPreview?id=999')
479+
480+
if(Cypress.env('DEPLOY') === 'local') {
481+
cy.makeCookiesWorkWithHttpAndReload()
482+
}
483+
484+
cy.get('h1').should('contain', 'Person #999')
485+
cy.get('p').should('contain', 'Sebastian Lacause')
486+
})
487+
488+
it('can move in and out of preview mode', () => {
489+
cy.visit('/api/enterPreview?id=999')
490+
491+
if(Cypress.env('DEPLOY') === 'local') {
492+
cy.makeCookiesWorkWithHttpAndReload()
493+
}
494+
495+
cy.get('h1').should('contain', 'Person #999')
496+
cy.get('p').should('contain', 'Sebastian Lacause')
497+
498+
cy.contains("Go back home").click()
499+
500+
// Verify that we're still in preview mode
501+
cy.contains("previewTest/222").click()
502+
cy.get('h1').should('contain', 'Person #222')
503+
cy.get('p').should('contain', 'Corey Lof')
504+
505+
// Exit preview mode
506+
cy.visit('/api/exitPreview')
507+
508+
// Verify that we're no longer in preview mode
509+
cy.contains("previewTest/222").click()
510+
cy.get('h1').should('contain', 'Show #222')
511+
cy.get('p').should('contain', 'Happyland')
512+
})
513+
})
514+
459515
describe('pre-rendered HTML pages', () => {
460516
context('with static route', () => {
461517
it('renders', () => {

cypress/support/commands.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,20 @@ Cypress.Commands.add("ssr", (url) => {
1313
// Verify that there are no scripts present
1414
cy.get('script').should('not.exist')
1515
})
16+
17+
// NextJS sends cookies with secure: true and Cypress does not send them to
18+
// the browser, because we are making requests to http:localhost:8888.
19+
// I briefly considered proxy-ing all requests via https using local-ssl-proxy
20+
// or similar, but I would prefer sticking as closely to `netlify dev` as
21+
// possible. Thus, this command to make tests with preview cookies work.
22+
Cypress.Commands.add('makeCookiesWorkWithHttpAndReload', () => {
23+
// First, remove secure attribute from all cookies
24+
cy.getCookies().then(cookies => (
25+
cookies.forEach(({ name, value, secure, sameSite, ...options }) =>
26+
cy.setCookie(name, value, options)
27+
)
28+
))
29+
30+
// Then reload the page (with our new, "non-secure" cookies)
31+
cy.reload()
32+
})

lib/netlifyFunctionTemplate.js

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,10 @@ const callbackHandler = callback => (
1212
// The callbackHandler wraps the callback
1313
(argument, response) => {
1414

15-
// Convert multi-value headers to plain headers, because Netlify does not
16-
// support multi-value headers.
17-
// See: https://github.com/netlify/cli/issues/923
18-
response.headers = {}
19-
Object.keys(response.multiValueHeaders).forEach(key => {
20-
response.headers[key] = response.multiValueHeaders[key][0]
21-
})
22-
delete response.multiValueHeaders
23-
2415
// Convert header values to string. Netlify does not support integers as
2516
// header values. See: https://github.com/netlify/cli/issues/451
26-
Object.keys(response.headers).forEach(key => {
27-
response.headers[key] = String(response.headers[key])
17+
Object.keys(response.multiValueHeaders).forEach(key => {
18+
response.multiValueHeaders[key] = response.multiValueHeaders[key].map(value => String(value))
2819
})
2920

3021
// Invoke callback
@@ -33,6 +24,24 @@ const callbackHandler = callback => (
3324
)
3425

3526
exports.handler = (event, context, callback) => {
27+
// In netlify dev, we currently do not receive headers as multi value headers.
28+
// So we manually set them from the plain headers. This should become
29+
// redundant as soon as https://github.com/netlify/cli/pull/938 is published.
30+
if(!event.hasOwnProperty('multiValueHeaders')) {
31+
event.multiValueHeaders = {}
32+
Object.keys(event.headers).forEach(key => {
33+
event.multiValueHeaders[key] = [event.headers[key]]
34+
})
35+
}
36+
37+
// In netlify dev, we currently do not receive query string as multi value
38+
// query string. So we manually set it from the plain query string. This
39+
// should become redundant as soon as https://github.com/netlify/cli/pull/938
40+
//is published.
41+
if(!event.hasOwnProperty('multiValueQueryStringParameters')) {
42+
event.multiValueQueryStringParameters = event.queryStringParameters
43+
}
44+
3645
// Get the request URL
3746
const { path } = event
3847
console.log("[request]", path)
@@ -43,8 +52,6 @@ exports.handler = (event, context, callback) => {
4352
...event,
4453
// Required. Otherwise, compat() will complain
4554
requestContext: {},
46-
// Pass query string parameters to NextJS
47-
multiValueQueryStringParameters: event.queryStringParameters
4855
},
4956
context,
5057
// Wrap the Netlify callback, so that we can resolve differences between

0 commit comments

Comments
 (0)