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

fix: convert HeadersInit to sequence/dictionary correctly #2784

Merged
merged 1 commit into from
Feb 20, 2024
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
6 changes: 4 additions & 2 deletions lib/fetch/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,10 @@ Object.defineProperties(Headers.prototype, {

webidl.converters.HeadersInit = function (V) {
if (webidl.util.Type(V) === 'Object') {
if (V[Symbol.iterator]) {
Copy link
Member Author

@KhafraDev KhafraDev Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there are two bugs here:

  • The existence of a Symbol.iterator does not mean a value is a sequence.
  • Accessing Symbol.iterator this way is detectable via Proxy (or actually... just a getter). Once we pass V to the sequence converter it once again accesses V[Symbol.iterator].

return webidl.converters['sequence<sequence<ByteString>>'](V)
const iterator = Reflect.get(V, Symbol.iterator)

if (typeof iterator === 'function') {
return webidl.converters['sequence<sequence<ByteString>>'](V, iterator.bind(V))
}

return webidl.converters['record<ByteString, ByteString>'](V)
Expand Down
8 changes: 4 additions & 4 deletions lib/fetch/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ webidl.util.IntegerPart = function (n) {

// https://webidl.spec.whatwg.org/#es-sequence
webidl.sequenceConverter = function (converter) {
return (V) => {
return (V, Iterable) => {
// 1. If Type(V) is not Object, throw a TypeError.
if (webidl.util.Type(V) !== 'Object') {
throw webidl.errors.exception({
Expand All @@ -229,7 +229,7 @@ webidl.sequenceConverter = function (converter) {

// 2. Let method be ? GetMethod(V, @@iterator).
/** @type {Generator} */
const method = V?.[Symbol.iterator]?.()
const method = typeof Iterable === 'function' ? Iterable() : V?.[Symbol.iterator]?.()
const seq = []

// 3. If method is undefined, throw a TypeError.
Expand Down Expand Up @@ -273,8 +273,8 @@ webidl.recordConverter = function (keyConverter, valueConverter) {
const result = {}

if (!types.isProxy(O)) {
// Object.keys only returns enumerable properties
const keys = Object.keys(O)
// 1. Let desc be ? O.[[GetOwnProperty]](key).
const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although Object.keys returns enumerable string properties, it didn't include enumerable symbol properties. This is only noticeable when doing something along the lines of new Headers({ [Symbol.iterator]: null }).


for (const key of keys) {
// 1. Let typedKey be key converted to an IDL value of type K.
Expand Down
27 changes: 27 additions & 0 deletions test/fetch/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,30 @@ test('When the value is updated, update the cache', (t) => {
headers.append('d', 'd')
deepStrictEqual([...headers], [...expected, ['d', 'd']])
})

test('Symbol.iterator is only accessed once', (t) => {
const { ok } = tspl(t, { plan: 1 })

const dict = new Proxy({}, {
get () {
ok(true)

return function * () {}
}
})

new Headers(dict) // eslint-disable-line no-new
})

test('Invalid Symbol.iterators', (t) => {
const { throws } = tspl(t, { plan: 3 })

throws(() => new Headers({ [Symbol.iterator]: null }), TypeError)
throws(() => new Headers({ [Symbol.iterator]: undefined }), TypeError)
throws(() => {
const obj = { [Symbol.iterator]: null }
Object.defineProperty(obj, Symbol.iterator, { enumerable: false })

new Headers(obj) // eslint-disable-line no-new
Copy link
Member Author

@KhafraDev KhafraDev Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This throws because Symbol.iterator can't be converted to a string rather than being an invalid iterator. Technically still a bug, but this also happens in Chrome so I assume no one in the history of mankind will ever notice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in, we can't implement the spec steps stating to ignore non-enumerable properties because then this case wouldn't throw an error.

}, TypeError)
})
2 changes: 1 addition & 1 deletion types/webidl.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
type Converter<T> = (object: unknown) => T

type SequenceConverter<T> = (object: unknown) => T[]
type SequenceConverter<T> = (object: unknown, iterable?: IterableIterator<T>) => T[]

type RecordConverter<K extends string, V> = (object: unknown) => Record<K, V>

Expand Down
Loading