diff --git a/.changeset/sharp-moose-perform.md b/.changeset/sharp-moose-perform.md new file mode 100644 index 000000000..2ade9c227 --- /dev/null +++ b/.changeset/sharp-moose-perform.md @@ -0,0 +1,5 @@ +--- +'@astrojs/netlify': patch +--- + +Fixes setting multiple cookies with the Netlify adapter diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index 474cac4b6..bdfb78b8e 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -33,14 +33,37 @@ export const createExports = (manifest: SSRManifest, args: Args) => { }; } - const response = await app.render(request); + const response: Response = await app.render(request); const responseBody = await response.text(); - return { - statusCode: 200, - headers: Object.fromEntries(response.headers.entries()), + const responseHeaders = Object.fromEntries(response.headers.entries()); + const fnResponse: any = { + statusCode: response.status, + headers: responseHeaders, body: responseBody, }; + + // Special-case set-cookie which has to be set an different way :/ + // The fetch API does not have a way to get multiples of a single header, but instead concatenates + // them. There are non-standard ways to do it, and node-fetch gives us headers.raw() + // See https://github.com/whatwg/fetch/issues/973 for discussion + if (response.headers.has('set-cookie') && 'raw' in response.headers) { + // Node fetch allows you to get the raw headers, which includes multiples of the same type. + // This is needed because Set-Cookie *must* be called for each cookie, and can't be + // concatenated together. + type HeadersWithRaw = Headers & { + raw: () => Record; + }; + + const rawPacked = (response.headers as HeadersWithRaw).raw(); + if('set-cookie' in rawPacked) { + fnResponse.multiValueHeaders = { + 'set-cookie': rawPacked['set-cookie'] + } + } + } + + return fnResponse; }; return { handler }; diff --git a/packages/integrations/netlify/test/cookies.test.js b/packages/integrations/netlify/test/cookies.test.js new file mode 100644 index 000000000..a8b304f56 --- /dev/null +++ b/packages/integrations/netlify/test/cookies.test.js @@ -0,0 +1,50 @@ +import { expect } from 'chai'; +import { load as cheerioLoad } from 'cheerio'; +import { loadFixture } from '../../../astro/test/test-utils.js'; +import netlifyAdapter from '../dist/index.js'; +import { fileURLToPath } from 'url'; + +describe('Cookies', () => { + /** @type {import('../../../astro/test/test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/cookies/', import.meta.url).toString(), + experimental: { + ssr: true, + }, + adapter: netlifyAdapter({ + dist: new URL('./fixtures/cookies/dist/', import.meta.url), + }), + site: `http://example.com`, + vite: { + resolve: { + alias: { + '@astrojs/netlify/netlify-functions.js': fileURLToPath( + new URL('../dist/netlify-functions.js', import.meta.url) + ), + }, + }, + }, + }); + await fixture.build(); + }); + + it('Can set multiple', async () => { + const entryURL = new URL('./fixtures/cookies/dist/functions/entry.mjs', import.meta.url); + const { handler } = await import(entryURL); + const resp = await handler({ + httpMethod: 'POST', + headers: {}, + rawUrl: 'http://example.com/login', + body: '{}', + isBase64Encoded: false + }); + expect(resp.statusCode).to.equal(301); + expect(resp.headers.location).to.equal('/'); + expect(resp.multiValueHeaders).to.be.deep.equal({ + 'set-cookie': [ 'foo=foo; HttpOnly', 'bar=bar; HttpOnly' ] + }); + }); +}); diff --git a/packages/integrations/netlify/test/fixtures/cookies/src/pages/index.astro b/packages/integrations/netlify/test/fixtures/cookies/src/pages/index.astro new file mode 100644 index 000000000..53e029f04 --- /dev/null +++ b/packages/integrations/netlify/test/fixtures/cookies/src/pages/index.astro @@ -0,0 +1,6 @@ + +Testing + +

Testing

+ + diff --git a/packages/integrations/netlify/test/fixtures/cookies/src/pages/login.js b/packages/integrations/netlify/test/fixtures/cookies/src/pages/login.js new file mode 100644 index 000000000..a9ca52f69 --- /dev/null +++ b/packages/integrations/netlify/test/fixtures/cookies/src/pages/login.js @@ -0,0 +1,12 @@ + +export function post() { + const headers = new Headers(); + headers.append('Set-Cookie', `foo=foo; HttpOnly`); + headers.append('Set-Cookie', `bar=bar; HttpOnly`); + headers.append('Location', '/'); + + return new Response('', { + status: 301, + headers, + }); +}