Skip to content

Commit c4f9a4f

Browse files
KhafraDevcrysmags
authored andcommitted
feat: add Headers.prototype.getSetCookie (nodejs#1915)
1 parent bd284fa commit c4f9a4f

File tree

4 files changed

+290
-9
lines changed

4 files changed

+290
-9
lines changed

lib/cookies/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ function getSetCookies (headers) {
8383
return []
8484
}
8585

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

8990
/**

lib/fetch/headers.js

+59-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
isValidHeaderValue
1212
} = require('./util')
1313
const { webidl } = require('./webidl')
14+
const assert = require('assert')
1415

1516
const kHeadersMap = Symbol('headers map')
1617
const kHeadersSortedMap = Symbol('headers map sorted')
@@ -115,7 +116,7 @@ class HeadersList {
115116

116117
if (lowercaseName === 'set-cookie') {
117118
this.cookies ??= []
118-
this.cookies.push([name, value])
119+
this.cookies.push(value)
119120
}
120121
}
121122

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

127128
if (lowercaseName === 'set-cookie') {
128-
this.cookies = [[name, value]]
129+
this.cookies = [value]
129130
}
130131

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

387+
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
388+
getSetCookie () {
389+
webidl.brandCheck(this, Headers)
390+
391+
// 1. If this’s header list does not contain `Set-Cookie`, then return « ».
392+
// 2. Return the values of all headers in this’s header list whose name is
393+
// a byte-case-insensitive match for `Set-Cookie`, in order.
394+
395+
return this[kHeadersList].cookies ?? []
396+
}
397+
398+
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
386399
get [kHeadersSortedMap] () {
387-
if (!this[kHeadersList][kHeadersSortedMap]) {
388-
this[kHeadersList][kHeadersSortedMap] = new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1))
400+
if (this[kHeadersList][kHeadersSortedMap]) {
401+
return this[kHeadersList][kHeadersSortedMap]
389402
}
390-
return this[kHeadersList][kHeadersSortedMap]
403+
404+
// 1. Let headers be an empty list of headers with the key being the name
405+
// and value the value.
406+
const headers = []
407+
408+
// 2. Let names be the result of convert header names to a sorted-lowercase
409+
// set with all the names of the headers in list.
410+
const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
411+
const cookies = this[kHeadersList].cookies
412+
413+
// 3. For each name of names:
414+
for (const [name, value] of names) {
415+
// 1. If name is `set-cookie`, then:
416+
if (name === 'set-cookie') {
417+
// 1. Let values be a list of all values of headers in list whose name
418+
// is a byte-case-insensitive match for name, in order.
419+
420+
// 2. For each value of values:
421+
// 1. Append (name, value) to headers.
422+
for (const value of cookies) {
423+
headers.push([name, value])
424+
}
425+
} else {
426+
// 2. Otherwise:
427+
428+
// 1. Let value be the result of getting name from list.
429+
430+
// 2. Assert: value is non-null.
431+
assert(value !== null)
432+
433+
// 3. Append (name, value) to headers.
434+
headers.push([name, value])
435+
}
436+
}
437+
438+
this[kHeadersList][kHeadersSortedMap] = headers
439+
440+
// 4. Return headers.
441+
return headers
391442
}
392443

393444
keys () {
394445
webidl.brandCheck(this, Headers)
395446

396447
return makeIterator(
397-
() => [...this[kHeadersSortedMap].entries()],
448+
() => [...this[kHeadersSortedMap].values()],
398449
'Headers',
399450
'key'
400451
)
@@ -404,7 +455,7 @@ class Headers {
404455
webidl.brandCheck(this, Headers)
405456

406457
return makeIterator(
407-
() => [...this[kHeadersSortedMap].entries()],
458+
() => [...this[kHeadersSortedMap].values()],
408459
'Headers',
409460
'value'
410461
)
@@ -414,7 +465,7 @@ class Headers {
414465
webidl.brandCheck(this, Headers)
415466

416467
return makeIterator(
417-
() => [...this[kHeadersSortedMap].entries()],
468+
() => [...this[kHeadersSortedMap].values()],
418469
'Headers',
419470
'key+value'
420471
)

test/wpt/status/fetch.status.json

+5
Original file line numberDiff line numberDiff line change
@@ -206,5 +206,10 @@
206206
"fetch() with value %1E",
207207
"fetch() with value %1F"
208208
]
209+
},
210+
"header-setcookie.any.js": {
211+
"fail": [
212+
"Set-Cookie is a forbidden response header"
213+
]
209214
}
210215
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// META: title=Headers set-cookie special cases
2+
// META: global=window,worker
3+
4+
const headerList = [
5+
["set-cookie", "foo=bar"],
6+
["Set-Cookie", "fizz=buzz; domain=example.com"],
7+
];
8+
9+
const setCookie2HeaderList = [
10+
["set-cookie2", "foo2=bar2"],
11+
["Set-Cookie2", "fizz2=buzz2; domain=example2.com"],
12+
];
13+
14+
function assert_nested_array_equals(actual, expected) {
15+
assert_equals(actual.length, expected.length, "Array length is not equal");
16+
for (let i = 0; i < expected.length; i++) {
17+
assert_array_equals(actual[i], expected[i]);
18+
}
19+
}
20+
21+
test(function () {
22+
const headers = new Headers(headerList);
23+
assert_equals(
24+
headers.get("set-cookie"),
25+
"foo=bar, fizz=buzz; domain=example.com",
26+
);
27+
}, "Headers.prototype.get combines set-cookie headers in order");
28+
29+
test(function () {
30+
const headers = new Headers(headerList);
31+
const list = [...headers];
32+
assert_nested_array_equals(list, [
33+
["set-cookie", "foo=bar"],
34+
["set-cookie", "fizz=buzz; domain=example.com"],
35+
]);
36+
}, "Headers iterator does not combine set-cookie headers");
37+
38+
test(function () {
39+
const headers = new Headers(setCookie2HeaderList);
40+
const list = [...headers];
41+
assert_nested_array_equals(list, [
42+
["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
43+
]);
44+
}, "Headers iterator does not special case set-cookie2 headers");
45+
46+
test(function () {
47+
const headers = new Headers([...headerList, ...setCookie2HeaderList]);
48+
const list = [...headers];
49+
assert_nested_array_equals(list, [
50+
["set-cookie", "foo=bar"],
51+
["set-cookie", "fizz=buzz; domain=example.com"],
52+
["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
53+
]);
54+
}, "Headers iterator does not combine set-cookie & set-cookie2 headers");
55+
56+
test(function () {
57+
// Values are in non alphabetic order, and the iterator should yield in the
58+
// headers in the exact order of the input.
59+
const headers = new Headers([
60+
["set-cookie", "z=z"],
61+
["set-cookie", "a=a"],
62+
["set-cookie", "n=n"],
63+
]);
64+
const list = [...headers];
65+
assert_nested_array_equals(list, [
66+
["set-cookie", "z=z"],
67+
["set-cookie", "a=a"],
68+
["set-cookie", "n=n"],
69+
]);
70+
}, "Headers iterator preserves set-cookie ordering");
71+
72+
test(
73+
function () {
74+
const headers = new Headers([
75+
["xylophone-header", "1"],
76+
["best-header", "2"],
77+
["set-cookie", "3"],
78+
["a-cool-header", "4"],
79+
["set-cookie", "5"],
80+
["a-cool-header", "6"],
81+
["best-header", "7"],
82+
]);
83+
const list = [...headers];
84+
assert_nested_array_equals(list, [
85+
["a-cool-header", "4, 6"],
86+
["best-header", "2, 7"],
87+
["set-cookie", "3"],
88+
["set-cookie", "5"],
89+
["xylophone-header", "1"],
90+
]);
91+
},
92+
"Headers iterator preserves per header ordering, but sorts keys alphabetically",
93+
);
94+
95+
test(
96+
function () {
97+
const headers = new Headers([
98+
["xylophone-header", "7"],
99+
["best-header", "6"],
100+
["set-cookie", "5"],
101+
["a-cool-header", "4"],
102+
["set-cookie", "3"],
103+
["a-cool-header", "2"],
104+
["best-header", "1"],
105+
]);
106+
const list = [...headers];
107+
assert_nested_array_equals(list, [
108+
["a-cool-header", "4, 2"],
109+
["best-header", "6, 1"],
110+
["set-cookie", "5"],
111+
["set-cookie", "3"],
112+
["xylophone-header", "7"],
113+
]);
114+
},
115+
"Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)",
116+
);
117+
118+
test(function () {
119+
const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]);
120+
const iterator = headers[Symbol.iterator]();
121+
assert_array_equals(iterator.next().value, ["fizz", "buzz"]);
122+
headers.append("Set-Cookie", "a=b");
123+
assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
124+
headers.append("Accept", "text/html");
125+
assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
126+
assert_array_equals(iterator.next().value, ["x-header", "test"]);
127+
headers.append("set-cookie", "c=d");
128+
assert_array_equals(iterator.next().value, ["x-header", "test"]);
129+
assert_true(iterator.next().done);
130+
}, "Headers iterator is correctly updated with set-cookie changes");
131+
132+
test(function () {
133+
const headers = new Headers(headerList);
134+
assert_true(headers.has("sEt-cOoKiE"));
135+
}, "Headers.prototype.has works for set-cookie");
136+
137+
test(function () {
138+
const headers = new Headers(setCookie2HeaderList);
139+
headers.append("set-Cookie", "foo=bar");
140+
headers.append("sEt-cOoKiE", "fizz=buzz");
141+
const list = [...headers];
142+
assert_nested_array_equals(list, [
143+
["set-cookie", "foo=bar"],
144+
["set-cookie", "fizz=buzz"],
145+
["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
146+
]);
147+
}, "Headers.prototype.append works for set-cookie");
148+
149+
test(function () {
150+
const headers = new Headers(headerList);
151+
headers.set("set-cookie", "foo2=bar2");
152+
const list = [...headers];
153+
assert_nested_array_equals(list, [
154+
["set-cookie", "foo2=bar2"],
155+
]);
156+
}, "Headers.prototype.set works for set-cookie");
157+
158+
test(function () {
159+
const headers = new Headers(headerList);
160+
headers.delete("set-Cookie");
161+
const list = [...headers];
162+
assert_nested_array_equals(list, []);
163+
}, "Headers.prototype.delete works for set-cookie");
164+
165+
test(function () {
166+
const headers = new Headers();
167+
assert_array_equals(headers.getSetCookie(), []);
168+
}, "Headers.prototype.getSetCookie with no headers present");
169+
170+
test(function () {
171+
const headers = new Headers([headerList[0]]);
172+
assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
173+
}, "Headers.prototype.getSetCookie with one header");
174+
175+
test(function () {
176+
const headers = new Headers({ "Set-Cookie": "foo=bar" });
177+
assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
178+
}, "Headers.prototype.getSetCookie with one header created from an object");
179+
180+
test(function () {
181+
const headers = new Headers(headerList);
182+
assert_array_equals(headers.getSetCookie(), [
183+
"foo=bar",
184+
"fizz=buzz; domain=example.com",
185+
]);
186+
}, "Headers.prototype.getSetCookie with multiple headers");
187+
188+
test(function () {
189+
const headers = new Headers([["set-cookie", ""]]);
190+
assert_array_equals(headers.getSetCookie(), [""]);
191+
}, "Headers.prototype.getSetCookie with an empty header");
192+
193+
test(function () {
194+
const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]);
195+
assert_array_equals(headers.getSetCookie(), ["x", "x"]);
196+
}, "Headers.prototype.getSetCookie with two equal headers");
197+
198+
test(function () {
199+
const headers = new Headers([
200+
["set-cookie2", "x"],
201+
["set-cookie", "y"],
202+
["set-cookie2", "z"],
203+
]);
204+
assert_array_equals(headers.getSetCookie(), ["y"]);
205+
}, "Headers.prototype.getSetCookie ignores set-cookie2 headers");
206+
207+
test(function () {
208+
// Values are in non alphabetic order, and the iterator should yield in the
209+
// headers in the exact order of the input.
210+
const headers = new Headers([
211+
["set-cookie", "z=z"],
212+
["set-cookie", "a=a"],
213+
["set-cookie", "n=n"],
214+
]);
215+
assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]);
216+
}, "Headers.prototype.getSetCookie preserves header ordering");
217+
218+
test(function () {
219+
const response = new Response();
220+
response.headers.append("Set-Cookie", "foo=bar");
221+
assert_array_equals(response.headers.getSetCookie(), []);
222+
response.headers.append("sEt-cOokIe", "bar=baz");
223+
assert_array_equals(response.headers.getSetCookie(), []);
224+
}, "Set-Cookie is a forbidden response header");

0 commit comments

Comments
 (0)