Skip to content

Commit

Permalink
feat: add Headers.prototype.getSetCookie (nodejs#1915)
Browse files Browse the repository at this point in the history
  • Loading branch information
KhafraDev authored and crysmags committed Feb 27, 2024
1 parent bd284fa commit c4f9a4f
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 9 deletions.
3 changes: 2 additions & 1 deletion lib/cookies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ function getSetCookies (headers) {
return []
}

return cookies.map((pair) => parseSetCookie(pair[1]))
// In older versions of undici, cookies is a list of name:value.
return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
}

/**
Expand Down
67 changes: 59 additions & 8 deletions lib/fetch/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
isValidHeaderValue
} = require('./util')
const { webidl } = require('./webidl')
const assert = require('assert')

const kHeadersMap = Symbol('headers map')
const kHeadersSortedMap = Symbol('headers map sorted')
Expand Down Expand Up @@ -115,7 +116,7 @@ class HeadersList {

if (lowercaseName === 'set-cookie') {
this.cookies ??= []
this.cookies.push([name, value])
this.cookies.push(value)
}
}

Expand All @@ -125,7 +126,7 @@ class HeadersList {
const lowercaseName = name.toLowerCase()

if (lowercaseName === 'set-cookie') {
this.cookies = [[name, value]]
this.cookies = [value]
}

// 1. If list contains name, then set the value of
Expand Down Expand Up @@ -383,18 +384,68 @@ class Headers {
return this[kHeadersList].set(name, value)
}

// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
getSetCookie () {
webidl.brandCheck(this, Headers)

// 1. If this’s header list does not contain `Set-Cookie`, then return « ».
// 2. Return the values of all headers in this’s header list whose name is
// a byte-case-insensitive match for `Set-Cookie`, in order.

return this[kHeadersList].cookies ?? []
}

// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
get [kHeadersSortedMap] () {
if (!this[kHeadersList][kHeadersSortedMap]) {
this[kHeadersList][kHeadersSortedMap] = new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1))
if (this[kHeadersList][kHeadersSortedMap]) {
return this[kHeadersList][kHeadersSortedMap]
}
return this[kHeadersList][kHeadersSortedMap]

// 1. Let headers be an empty list of headers with the key being the name
// and value the value.
const headers = []

// 2. Let names be the result of convert header names to a sorted-lowercase
// set with all the names of the headers in list.
const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
const cookies = this[kHeadersList].cookies

// 3. For each name of names:
for (const [name, value] of names) {
// 1. If name is `set-cookie`, then:
if (name === 'set-cookie') {
// 1. Let values be a list of all values of headers in list whose name
// is a byte-case-insensitive match for name, in order.

// 2. For each value of values:
// 1. Append (name, value) to headers.
for (const value of cookies) {
headers.push([name, value])
}
} else {
// 2. Otherwise:

// 1. Let value be the result of getting name from list.

// 2. Assert: value is non-null.
assert(value !== null)

// 3. Append (name, value) to headers.
headers.push([name, value])
}
}

this[kHeadersList][kHeadersSortedMap] = headers

// 4. Return headers.
return headers
}

keys () {
webidl.brandCheck(this, Headers)

return makeIterator(
() => [...this[kHeadersSortedMap].entries()],
() => [...this[kHeadersSortedMap].values()],
'Headers',
'key'
)
Expand All @@ -404,7 +455,7 @@ class Headers {
webidl.brandCheck(this, Headers)

return makeIterator(
() => [...this[kHeadersSortedMap].entries()],
() => [...this[kHeadersSortedMap].values()],
'Headers',
'value'
)
Expand All @@ -414,7 +465,7 @@ class Headers {
webidl.brandCheck(this, Headers)

return makeIterator(
() => [...this[kHeadersSortedMap].entries()],
() => [...this[kHeadersSortedMap].values()],
'Headers',
'key+value'
)
Expand Down
5 changes: 5 additions & 0 deletions test/wpt/status/fetch.status.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,10 @@
"fetch() with value %1E",
"fetch() with value %1F"
]
},
"header-setcookie.any.js": {
"fail": [
"Set-Cookie is a forbidden response header"
]
}
}
224 changes: 224 additions & 0 deletions test/wpt/tests/fetch/api/headers/header-setcookie.any.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// META: title=Headers set-cookie special cases
// META: global=window,worker

const headerList = [
["set-cookie", "foo=bar"],
["Set-Cookie", "fizz=buzz; domain=example.com"],
];

const setCookie2HeaderList = [
["set-cookie2", "foo2=bar2"],
["Set-Cookie2", "fizz2=buzz2; domain=example2.com"],
];

function assert_nested_array_equals(actual, expected) {
assert_equals(actual.length, expected.length, "Array length is not equal");
for (let i = 0; i < expected.length; i++) {
assert_array_equals(actual[i], expected[i]);
}
}

test(function () {
const headers = new Headers(headerList);
assert_equals(
headers.get("set-cookie"),
"foo=bar, fizz=buzz; domain=example.com",
);
}, "Headers.prototype.get combines set-cookie headers in order");

test(function () {
const headers = new Headers(headerList);
const list = [...headers];
assert_nested_array_equals(list, [
["set-cookie", "foo=bar"],
["set-cookie", "fizz=buzz; domain=example.com"],
]);
}, "Headers iterator does not combine set-cookie headers");

test(function () {
const headers = new Headers(setCookie2HeaderList);
const list = [...headers];
assert_nested_array_equals(list, [
["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
]);
}, "Headers iterator does not special case set-cookie2 headers");

test(function () {
const headers = new Headers([...headerList, ...setCookie2HeaderList]);
const list = [...headers];
assert_nested_array_equals(list, [
["set-cookie", "foo=bar"],
["set-cookie", "fizz=buzz; domain=example.com"],
["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
]);
}, "Headers iterator does not combine set-cookie & set-cookie2 headers");

test(function () {
// Values are in non alphabetic order, and the iterator should yield in the
// headers in the exact order of the input.
const headers = new Headers([
["set-cookie", "z=z"],
["set-cookie", "a=a"],
["set-cookie", "n=n"],
]);
const list = [...headers];
assert_nested_array_equals(list, [
["set-cookie", "z=z"],
["set-cookie", "a=a"],
["set-cookie", "n=n"],
]);
}, "Headers iterator preserves set-cookie ordering");

test(
function () {
const headers = new Headers([
["xylophone-header", "1"],
["best-header", "2"],
["set-cookie", "3"],
["a-cool-header", "4"],
["set-cookie", "5"],
["a-cool-header", "6"],
["best-header", "7"],
]);
const list = [...headers];
assert_nested_array_equals(list, [
["a-cool-header", "4, 6"],
["best-header", "2, 7"],
["set-cookie", "3"],
["set-cookie", "5"],
["xylophone-header", "1"],
]);
},
"Headers iterator preserves per header ordering, but sorts keys alphabetically",
);

test(
function () {
const headers = new Headers([
["xylophone-header", "7"],
["best-header", "6"],
["set-cookie", "5"],
["a-cool-header", "4"],
["set-cookie", "3"],
["a-cool-header", "2"],
["best-header", "1"],
]);
const list = [...headers];
assert_nested_array_equals(list, [
["a-cool-header", "4, 2"],
["best-header", "6, 1"],
["set-cookie", "5"],
["set-cookie", "3"],
["xylophone-header", "7"],
]);
},
"Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)",
);

test(function () {
const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]);
const iterator = headers[Symbol.iterator]();
assert_array_equals(iterator.next().value, ["fizz", "buzz"]);
headers.append("Set-Cookie", "a=b");
assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
headers.append("Accept", "text/html");
assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
assert_array_equals(iterator.next().value, ["x-header", "test"]);
headers.append("set-cookie", "c=d");
assert_array_equals(iterator.next().value, ["x-header", "test"]);
assert_true(iterator.next().done);
}, "Headers iterator is correctly updated with set-cookie changes");

test(function () {
const headers = new Headers(headerList);
assert_true(headers.has("sEt-cOoKiE"));
}, "Headers.prototype.has works for set-cookie");

test(function () {
const headers = new Headers(setCookie2HeaderList);
headers.append("set-Cookie", "foo=bar");
headers.append("sEt-cOoKiE", "fizz=buzz");
const list = [...headers];
assert_nested_array_equals(list, [
["set-cookie", "foo=bar"],
["set-cookie", "fizz=buzz"],
["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
]);
}, "Headers.prototype.append works for set-cookie");

test(function () {
const headers = new Headers(headerList);
headers.set("set-cookie", "foo2=bar2");
const list = [...headers];
assert_nested_array_equals(list, [
["set-cookie", "foo2=bar2"],
]);
}, "Headers.prototype.set works for set-cookie");

test(function () {
const headers = new Headers(headerList);
headers.delete("set-Cookie");
const list = [...headers];
assert_nested_array_equals(list, []);
}, "Headers.prototype.delete works for set-cookie");

test(function () {
const headers = new Headers();
assert_array_equals(headers.getSetCookie(), []);
}, "Headers.prototype.getSetCookie with no headers present");

test(function () {
const headers = new Headers([headerList[0]]);
assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
}, "Headers.prototype.getSetCookie with one header");

test(function () {
const headers = new Headers({ "Set-Cookie": "foo=bar" });
assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
}, "Headers.prototype.getSetCookie with one header created from an object");

test(function () {
const headers = new Headers(headerList);
assert_array_equals(headers.getSetCookie(), [
"foo=bar",
"fizz=buzz; domain=example.com",
]);
}, "Headers.prototype.getSetCookie with multiple headers");

test(function () {
const headers = new Headers([["set-cookie", ""]]);
assert_array_equals(headers.getSetCookie(), [""]);
}, "Headers.prototype.getSetCookie with an empty header");

test(function () {
const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]);
assert_array_equals(headers.getSetCookie(), ["x", "x"]);
}, "Headers.prototype.getSetCookie with two equal headers");

test(function () {
const headers = new Headers([
["set-cookie2", "x"],
["set-cookie", "y"],
["set-cookie2", "z"],
]);
assert_array_equals(headers.getSetCookie(), ["y"]);
}, "Headers.prototype.getSetCookie ignores set-cookie2 headers");

test(function () {
// Values are in non alphabetic order, and the iterator should yield in the
// headers in the exact order of the input.
const headers = new Headers([
["set-cookie", "z=z"],
["set-cookie", "a=a"],
["set-cookie", "n=n"],
]);
assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]);
}, "Headers.prototype.getSetCookie preserves header ordering");

test(function () {
const response = new Response();
response.headers.append("Set-Cookie", "foo=bar");
assert_array_equals(response.headers.getSetCookie(), []);
response.headers.append("sEt-cOokIe", "bar=baz");
assert_array_equals(response.headers.getSetCookie(), []);
}, "Set-Cookie is a forbidden response header");

0 comments on commit c4f9a4f

Please sign in to comment.