Skip to content

Commit 2787b64

Browse files
authored
fix(fetch): re-calculate iterator after each iteration (nodejs#1706)
1 parent 8d6ddb7 commit 2787b64

File tree

5 files changed

+175
-65
lines changed

5 files changed

+175
-65
lines changed

lib/fetch/formdata.js

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,9 @@ class FormData {
206206
}
207207

208208
return makeIterator(
209-
makeIterable(this[kState], 'entries'),
210-
'FormData'
209+
() => this[kState].map(pair => [pair.name, pair.value]),
210+
'FormData',
211+
'key+value'
211212
)
212213
}
213214

@@ -217,8 +218,9 @@ class FormData {
217218
}
218219

219220
return makeIterator(
220-
makeIterable(this[kState], 'keys'),
221-
'FormData'
221+
() => this[kState].map(pair => [pair.name, pair.value]),
222+
'FormData',
223+
'key'
222224
)
223225
}
224226

@@ -228,8 +230,9 @@ class FormData {
228230
}
229231

230232
return makeIterator(
231-
makeIterable(this[kState], 'values'),
232-
'FormData'
233+
() => this[kState].map(pair => [pair.name, pair.value]),
234+
'FormData',
235+
'value'
233236
)
234237
}
235238

@@ -304,18 +307,4 @@ function makeEntry (name, value, filename) {
304307
return { name, value }
305308
}
306309

307-
function * makeIterable (entries, type) {
308-
// The value pairs to iterate over are this’s entry list’s entries
309-
// with the key being the name and the value being the value.
310-
for (const { name, value } of entries) {
311-
if (type === 'entries') {
312-
yield [name, value]
313-
} else if (type === 'values') {
314-
yield value
315-
} else {
316-
yield name
317-
}
318-
}
319-
}
320-
321310
module.exports = { FormData }

lib/fetch/headers.js

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,10 @@ class HeadersList {
147147
return this[kHeadersMap].has(name)
148148
}
149149

150-
keys () {
151-
return this[kHeadersMap].keys()
152-
}
153-
154-
values () {
155-
return this[kHeadersMap].values()
156-
}
157-
158-
entries () {
159-
return this[kHeadersMap].entries()
160-
}
161-
162-
[Symbol.iterator] () {
163-
return this[kHeadersMap][Symbol.iterator]()
150+
* [Symbol.iterator] () {
151+
for (const pair of this[kHeadersMap]) {
152+
yield pair
153+
}
164154
}
165155
}
166156

@@ -413,23 +403,35 @@ class Headers {
413403
throw new TypeError('Illegal invocation')
414404
}
415405

416-
return makeIterator(this[kHeadersSortedMap].keys(), 'Headers')
406+
return makeIterator(
407+
() => [...this[kHeadersSortedMap].entries()],
408+
'Headers',
409+
'key'
410+
)
417411
}
418412

419413
values () {
420414
if (!(this instanceof Headers)) {
421415
throw new TypeError('Illegal invocation')
422416
}
423417

424-
return makeIterator(this[kHeadersSortedMap].values(), 'Headers')
418+
return makeIterator(
419+
() => [...this[kHeadersSortedMap].entries()],
420+
'Headers',
421+
'value'
422+
)
425423
}
426424

427425
entries () {
428426
if (!(this instanceof Headers)) {
429427
throw new TypeError('Illegal invocation')
430428
}
431429

432-
return makeIterator(this[kHeadersSortedMap].entries(), 'Headers')
430+
return makeIterator(
431+
() => [...this[kHeadersSortedMap].entries()],
432+
'Headers',
433+
'key+value'
434+
)
433435
}
434436

435437
/**

lib/fetch/util.js

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -684,17 +684,61 @@ function serializeJavascriptValueToJSONString (value) {
684684
// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
685685
const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
686686

687-
// https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
688-
function makeIterator (iterator, name) {
687+
/**
688+
* @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
689+
* @param {() => unknown[]} iterator
690+
* @param {string} name name of the instance
691+
* @param {'key'|'value'|'key+value'} kind
692+
*/
693+
function makeIterator (iterator, name, kind) {
694+
const object = {
695+
index: 0,
696+
kind,
697+
target: iterator
698+
}
699+
689700
const i = {
690701
next () {
702+
// 1. Let interface be the interface for which the iterator prototype object exists.
703+
704+
// 2. Let thisValue be the this value.
705+
706+
// 3. Let object be ? ToObject(thisValue).
707+
708+
// 4. If object is a platform object, then perform a security
709+
// check, passing:
710+
711+
// 5. If object is not a default iterator object for interface,
712+
// then throw a TypeError.
691713
if (Object.getPrototypeOf(this) !== i) {
692714
throw new TypeError(
693715
`'next' called on an object that does not implement interface ${name} Iterator.`
694716
)
695717
}
696718

697-
return iterator.next()
719+
// 6. Let index be object’s index.
720+
// 7. Let kind be object’s kind.
721+
// 8. Let values be object’s target's value pairs to iterate over.
722+
const { index, kind, target } = object
723+
const values = target()
724+
725+
// 9. Let len be the length of values.
726+
const len = values.length
727+
728+
// 10. If index is greater than or equal to len, then return
729+
// CreateIterResultObject(undefined, true).
730+
if (index >= len) {
731+
return { value: undefined, done: true }
732+
}
733+
734+
// 11. Let pair be the entry in values at index index.
735+
const pair = values[index]
736+
737+
// 12. Set object’s index to index + 1.
738+
object.index = index + 1
739+
740+
// 13. Return the iterator result for pair and kind.
741+
return iteratorResult(pair, kind)
698742
},
699743
// The class string of an iterator prototype object for a given interface is the
700744
// result of concatenating the identifier of the interface and the string " Iterator".
@@ -708,6 +752,48 @@ function makeIterator (iterator, name) {
708752
return Object.setPrototypeOf({}, i)
709753
}
710754

755+
// https://webidl.spec.whatwg.org/#iterator-result
756+
function iteratorResult (pair, kind) {
757+
let result
758+
759+
// 1. Let result be a value determined by the value of kind:
760+
switch (kind) {
761+
case 'key': {
762+
// 1. Let idlKey be pair’s key.
763+
// 2. Let key be the result of converting idlKey to an
764+
// ECMAScript value.
765+
// 3. result is key.
766+
result = pair[0]
767+
break
768+
}
769+
case 'value': {
770+
// 1. Let idlValue be pair’s value.
771+
// 2. Let value be the result of converting idlValue to
772+
// an ECMAScript value.
773+
// 3. result is value.
774+
result = pair[1]
775+
break
776+
}
777+
case 'key+value': {
778+
// 1. Let idlKey be pair’s key.
779+
// 2. Let idlValue be pair’s value.
780+
// 3. Let key be the result of converting idlKey to an
781+
// ECMAScript value.
782+
// 4. Let value be the result of converting idlValue to
783+
// an ECMAScript value.
784+
// 5. Let array be ! ArrayCreate(2).
785+
// 6. Call ! CreateDataProperty(array, "0", key).
786+
// 7. Call ! CreateDataProperty(array, "1", value).
787+
// 8. result is array.
788+
result = pair
789+
break
790+
}
791+
}
792+
793+
// 2. Return CreateIterResultObject(result, false).
794+
return { value: result, done: false }
795+
}
796+
711797
/**
712798
* @see https://fetch.spec.whatwg.org/#body-fully-read
713799
*/

test/fetch/headers.js

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ tap.test('Headers forEach', t => {
329329
})
330330

331331
tap.test('Headers as Iterable', t => {
332-
t.plan(8)
332+
t.plan(7)
333333

334334
t.test('should freeze values while iterating', t => {
335335
t.plan(1)
@@ -338,8 +338,8 @@ tap.test('Headers as Iterable', t => {
338338
['bar', '456']
339339
]
340340
const expected = [
341-
['x-bar', '456'],
342-
['x-foo', '123']
341+
['foo', '123'],
342+
['x-x-bar', '456']
343343
]
344344
const headers = new Headers(init)
345345
for (const [key, val] of headers) {
@@ -349,28 +349,6 @@ tap.test('Headers as Iterable', t => {
349349
t.strictSame([...headers], expected)
350350
})
351351

352-
t.test('prevent infinite, continuous iteration', t => {
353-
t.plan(2)
354-
355-
const headers = new Headers({
356-
z: 1,
357-
y: 2,
358-
x: 3
359-
})
360-
361-
const order = []
362-
for (const [key] of headers) {
363-
order.push(key)
364-
headers.append(key + key, 1)
365-
}
366-
367-
t.strictSame(order, ['x', 'y', 'z'])
368-
t.strictSame(
369-
[...headers.keys()],
370-
['x', 'xx', 'y', 'yy', 'z', 'zz']
371-
)
372-
})
373-
374352
t.test('returns combined and sorted entries using .forEach()', t => {
375353
t.plan(8)
376354
const init = [

test/wpt/tests/fetch/api/headers/headers-basic.any.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,58 @@ test(function() {
218218
});
219219
assert_true(reference.next().done);
220220
}, "Check forEach method");
221+
222+
test(() => {
223+
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"});
224+
const actualKeys = [];
225+
const actualValues = [];
226+
for (const [header, value] of headers) {
227+
actualKeys.push(header);
228+
actualValues.push(value);
229+
headers.delete("foo");
230+
}
231+
assert_array_equals(actualKeys, ["bar", "baz"]);
232+
assert_array_equals(actualValues, ["0", "1"]);
233+
}, "Iteration skips elements removed while iterating");
234+
235+
test(() => {
236+
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
237+
const actualKeys = [];
238+
const actualValues = [];
239+
for (const [header, value] of headers) {
240+
actualKeys.push(header);
241+
actualValues.push(value);
242+
if (header === "baz")
243+
headers.delete("bar");
244+
}
245+
assert_array_equals(actualKeys, ["bar", "baz", "quux"]);
246+
assert_array_equals(actualValues, ["0", "1", "3"]);
247+
}, "Removing elements already iterated over causes an element to be skipped during iteration");
248+
249+
test(() => {
250+
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
251+
const actualKeys = [];
252+
const actualValues = [];
253+
for (const [header, value] of headers) {
254+
actualKeys.push(header);
255+
actualValues.push(value);
256+
if (header === "baz")
257+
headers.append("X-yZ", "4");
258+
}
259+
assert_array_equals(actualKeys, ["bar", "baz", "foo", "quux", "x-yz"]);
260+
assert_array_equals(actualValues, ["0", "1", "2", "3", "4"]);
261+
}, "Appending a value pair during iteration causes it to be reached during iteration");
262+
263+
test(() => {
264+
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
265+
const actualKeys = [];
266+
const actualValues = [];
267+
for (const [header, value] of headers) {
268+
actualKeys.push(header);
269+
actualValues.push(value);
270+
if (header === "baz")
271+
headers.append("abc", "-1");
272+
}
273+
assert_array_equals(actualKeys, ["bar", "baz", "baz", "foo", "quux"]);
274+
assert_array_equals(actualValues, ["0", "1", "1", "2", "3"]);
275+
}, "Prepending a value pair before the current element position causes it to be skipped during iteration and adds the current element a second time");

0 commit comments

Comments
 (0)