Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#7226 - fixes NodeJS adapter for multiple set-cookie headers (and other header issues) #7227

Merged
merged 5 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilled-rockets-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/node': minor
---

Fixes NodeJS adapter for multiple set-cookie headers and combining AstroCookies and Response.headers cookies
1 change: 1 addition & 0 deletions packages/integrations/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"astro": "workspace:^2.5.7"
},
"devDependencies": {
"@types/node": "^18.7.21",
"@types/send": "^0.17.1",
"@types/server-destroy": "^1.0.1",
"astro": "workspace:*",
Expand Down
40 changes: 40 additions & 0 deletions packages/integrations/node/src/createOutgoingHttpHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { OutgoingHttpHeaders } from 'http';

/**
* Takes in a nullable WebAPI Headers object and produces a NodeJS OutgoingHttpHeaders object suitable for usage
* with ServerResponse.writeHead(..) or ServerResponse.setHeader(..)
*
* @param webHeaders WebAPI Headers object
* @returns NodeJS OutgoingHttpHeaders object with multiple set-cookie handled as an array of values
*/
export const createOutgoingHttpHeaders = (webHeaders: Headers | undefined | null): OutgoingHttpHeaders | undefined => {
if (!webHeaders) {
return undefined;
}

// re-type to access Header.getSetCookie()
const headers = webHeaders as HeadersWithGetSetCookie;

// at this point, a multi-value'd set-cookie header is invalid (it was concatenated as a single CSV, which is not valid for set-cookie)
const nodeHeaders: OutgoingHttpHeaders = Object.fromEntries(headers.entries());

if (Object.keys(nodeHeaders).length === 0) {
return undefined;
}

// if there is > 1 set-cookie header, we have to fix it to be an array of values
if (headers.has('set-cookie')) {
const cookieHeaders = headers.getSetCookie();
if (cookieHeaders.length > 1) {
// the Headers.entries() API already normalized all header names to lower case so we can safely index this as 'set-cookie'
nodeHeaders['set-cookie'] = cookieHeaders;
}
}

return nodeHeaders;
};

interface HeadersWithGetSetCookie extends Headers {
// the @astrojs/webapi polyfill makes this available (as of [email protected]), but tsc doesn't pick it up on the built-in Headers type from DOM lib
getSetCookie(): string[];
}
9 changes: 7 additions & 2 deletions packages/integrations/node/src/nodeMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'http';
import type { Readable } from 'stream';
import { responseIterator } from './response-iterator';
import type { Options } from './types';
import { createOutgoingHttpHeaders } from './createOutgoingHttpHeaders';

export default function (app: NodeApp, mode: Options['mode']) {
return async function (
Expand Down Expand Up @@ -44,12 +45,16 @@ async function writeWebResponse(app: NodeApp, res: ServerResponse, webResponse:

if (app.setCookieHeaders) {
const setCookieHeaders: Array<string> = Array.from(app.setCookieHeaders(webResponse));

if (setCookieHeaders.length) {
res.setHeader('Set-Cookie', setCookieHeaders);
for (const setCookieHeader of setCookieHeaders) {
webResponse.headers.append('set-cookie', setCookieHeader);
}
}
}

res.writeHead(status, Object.fromEntries(headers.entries()));
const nodeHeaders = createOutgoingHttpHeaders(headers);
res.writeHead(status, nodeHeaders);
if (webResponse.body) {
try {
for await (const chunk of responseIterator(webResponse) as unknown as Readable) {
Expand Down
78 changes: 78 additions & 0 deletions packages/integrations/node/test/createOutgoingHttpHeaders.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect } from 'chai';

import { createOutgoingHttpHeaders } from '../dist/createOutgoingHttpHeaders.js';

describe('createOutgoingHttpHeaders', () => {

it('undefined input headers', async () => {
const result = createOutgoingHttpHeaders(undefined);
expect(result).to.equal(undefined);
});

it('null input headers', async () => {
const result = createOutgoingHttpHeaders(undefined);
expect(result).to.equal(undefined);
});

it('Empty Headers', async () => {
const headers = new Headers();
const result = createOutgoingHttpHeaders(headers);
expect(result).to.equal(undefined);
});

it('Headers with single key', async () => {
const headers = new Headers();
headers.append('x-test', 'hello world');
const result = createOutgoingHttpHeaders(headers);
expect(result).to.deep.equal({ 'x-test': 'hello world' });
});

it('Headers with multiple keys', async () => {
const headers = new Headers();
headers.append('x-test1', 'hello');
headers.append('x-test2', 'world');
const result = createOutgoingHttpHeaders(headers);
expect(result).to.deep.equal({ 'x-test1': 'hello', 'x-test2': 'world' });
});

it('Headers with multiple values (not set-cookie)', async () => {
const headers = new Headers();
headers.append('x-test', 'hello');
headers.append('x-test', 'world');
const result = createOutgoingHttpHeaders(headers);
expect(result).to.deep.equal({ 'x-test': 'hello, world' });
});

it('Headers with multiple values (set-cookie special case)', async () => {
const headers = new Headers();
headers.append('set-cookie', 'hello');
headers.append('set-cookie', 'world');
const result = createOutgoingHttpHeaders(headers);
expect(result).to.deep.equal({ 'set-cookie': ['hello', 'world'] });
});

it('Headers with multiple values (set-cookie case handling)', async () => {
const headers = new Headers();
headers.append('Set-cookie', 'hello');
headers.append('Set-Cookie', 'world');
const result = createOutgoingHttpHeaders(headers);
expect(result).to.deep.equal({ 'set-cookie': ['hello', 'world'] });
});

it('Headers with all use cases', async () => {
const headers = new Headers();
headers.append('x-single', 'single');
headers.append('x-triple', 'one');
headers.append('x-triple', 'two');
headers.append('x-triple', 'three');
headers.append('Set-cookie', 'hello');
headers.append('Set-Cookie', 'world');
const result = createOutgoingHttpHeaders(headers);
expect(result).to.deep.equal({
'x-single': 'single',
'x-triple': 'one, two, three',
'set-cookie': ['hello', 'world'],
});
});

});
9 changes: 9 additions & 0 deletions packages/integrations/node/test/fixtures/headers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/nodejs-headers",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
Astro.cookies.set('from1', 'astro1');
Astro.cookies.set('from2', 'astro2');
---
<p>hello world</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
Astro.cookies.set('from1', 'astro1');
---
<p>hello world</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
Astro.response.headers.append('set-cookie', 'from1=response1');
Astro.response.headers.append('set-cookie', 'from2=response2');
Astro.cookies.set('from3', 'astro1');
Astro.cookies.set('from4', 'astro2');
---
<p>hello world</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
Astro.response.headers.append('set-cookie', 'from1=response1');
Astro.cookies.set('from1', 'astro1');
---
<p>hello world</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
Astro.response.headers.append('set-cookie', 'from1=value1');
Astro.response.headers.append('set-cookie', 'from2=value2');
---
<p>hello world</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
Astro.response.headers.append('set-cookie', 'from1=value1');
---
<p>hello world</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { APIContext } from 'astro';

export async function get({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
cookies.set('from1', 'astro1');
cookies.set('from2', 'astro2');
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { APIContext } from 'astro';

export async function get({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
cookies.set('from1', 'astro1');
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { APIContext } from 'astro';

export async function get({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('set-cookie', 'from1=response1');
headers.append('set-cookie', 'from2=response2');
cookies.set('from3', 'astro1');
cookies.set('from4', 'astro2');
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { APIContext } from 'astro';

export async function get({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('set-cookie', 'from1=response1');
cookies.set('from1', 'astro1');
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export async function get({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('x-SINGLE', 'single');
headers.append('X-triple', 'one');
headers.append('x-Triple', 'two');
headers.append('x-TRIPLE', 'three');
headers.append('SET-cookie', 'hello1=world1');
headers.append('Set-Cookie', 'hello2=world2');
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export async function get({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('Set-Cookie', 'hello1=world1');
headers.append('SET-COOKIE', 'hello2=world2');
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function get({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('Set-Cookie', 'hello1=world1');
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export async function get({ request }: { request: Request }) {
const headers = new Headers();
return new Response('hello world', { headers });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function get({ request }: { request: Request }) {
return new Response('hello world', { headers: undefined });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function get({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('X-HELLO', 'world');
return new Response('hello world', { headers });
}
Loading