Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions .changeset/smooth-worlds-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@whatwg-node/node-fetch': patch
'@whatwg-node/server': patch
---

Fix handling multiple `set-cookie` headers correctly with `uWebSockets.js` integration
16 changes: 14 additions & 2 deletions packages/node-fetch/src/Headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ export class PonyfillHeaders implements Headers {
for (const [key, value] of this.headersInit) {
const normalizedKey = key.toLowerCase();
if (normalizedKey === 'set-cookie') {
this._setCookies.push(value);
if (Array.isArray(value)) {
this._setCookies.push(...value);
} else if (value != null) {
this._setCookies.push(value);
}
continue;
}
this._map.set(normalizedKey, value);
Expand All @@ -87,7 +91,11 @@ export class PonyfillHeaders implements Headers {
this.headersInit.forEach((value, key) => {
if (key === 'set-cookie') {
this._setCookies ||= [];
this._setCookies.push(value);
if (Array.isArray(value)) {
this._setCookies.push(...value);
} else if (value != null) {
this._setCookies.push(value);
}
return;
}
this._map!.set(key, value);
Expand All @@ -100,6 +108,10 @@ export class PonyfillHeaders implements Headers {
const normalizedKey = initKey.toLowerCase();
if (normalizedKey === 'set-cookie') {
this._setCookies ||= [];
if (Array.isArray(initValue)) {
this._setCookies.push(...initValue);
continue;
}
this._setCookies.push(initValue);
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export function sendNodeResponse(
);
} else {
// @ts-expect-error - setHeaders exist
if (serverResponse.setHeaders) {
if (serverResponse.setHeaders && globalThis.process?.versions?.node?.startsWith('2')) {
// @ts-expect-error - setHeaders exist
serverResponse.setHeaders(fetchResponse.headers);
} else {
Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/uwebsockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,15 @@ export function sendResponseToUwsOpts(
}
uwsResponse.cork(() => {
uwsResponse.writeStatus(`${fetchResponse.status} ${fetchResponse.statusText}`);
let isSetCookieHandled = false;
for (const [key, value] of fetchResponse.headers) {
// content-length causes an error with Node.js's fetch
if (key !== 'content-length') {
if (key === 'set-cookie') {
if (isSetCookieHandled) {
continue;
}
isSetCookieHandled = true;
const setCookies = fetchResponse.headers.getSetCookie?.();
if (setCookies) {
for (const setCookie of setCookies) {
Expand Down
30 changes: 30 additions & 0 deletions packages/server/test/cookies.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from '@jest/globals';
import { runTestsForEachFetchImpl } from './test-fetch';
import { runTestsForEachServerImpl } from './test-server';

describe('Cookies', () => {
runTestsForEachFetchImpl((_fetchImpl, { createServerAdapter, fetchAPI }) => {
runTestsForEachServerImpl(serverImpl => {
const skipIf = (condition: boolean) => (condition ? it.skip : it);
// Hapi has some issues with multiple Set-Cookie headers
skipIf(serverImpl.name === 'hapi')('should handle cookies correctly', async () => {
serverImpl.addOnceHandler(
createServerAdapter(() => {
const response = new fetchAPI.Response('OK');
response.headers.append('set-cookie', 'name=value0; SameSite=None; Secure');
response.headers.append('set-cookie', 'name=value1; SameSite=Strict; Secure');
return response;
}),
);
const response = await fetchAPI.fetch(serverImpl.url);
const cookies = response.headers.getSetCookie();
expect(cookies).toEqual([
'name=value0; SameSite=None; Secure',
'name=value1; SameSite=Strict; Secure',
]);
const resBody = await response.text();
expect(resBody).toBe('OK');
});
});
});
});
Loading