diff --git a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs index 2ff21e0b27..85844172be 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs @@ -1,32 +1,6 @@ use crate::webidl; use moli_url::search_params::{SearchParamPair, parse_search_params}; -struct UrlSearchParamsSequencePair(SearchParamPair); - -impl<'s> webidl::WebIdlConverter<'s> for UrlSearchParamsSequencePair { - type Options = webidl::StringOptions; - - fn convert( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, - context: webidl::Context, - options: &Self::Options, - ) -> Result { - let pair = as webidl::WebIdlConverter>::convert( - scope, value, context, options, - )?; - if pair.0.len() != 2 { - return Err(webidl::WebIdlError::custom_message( - "URLSearchParams sequence pairs must contain exactly two items", - )); - } - let mut values = pair.0.into_iter(); - let key = values.next().expect("validated sequence pair key").0; - let value = values.next().expect("validated sequence pair value").0; - Ok(Self((key, value))) - } -} - pub(super) fn url_search_params_pairs_from_constructor<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, @@ -35,7 +9,7 @@ pub(super) fn url_search_params_pairs_from_constructor<'s>( return Some(Vec::new()); } if let Ok(object) = v8::Local::::try_from(value) { - let sequence = match webidl::convert_optional_sequence::( + let sequence = match webidl::convert_optional_sequence::>( scope, value, webidl::Context::argument("URLSearchParams", 1), @@ -48,7 +22,21 @@ pub(super) fn url_search_params_pairs_from_constructor<'s>( } }; if let Some(sequence) = sequence { - return Some(sequence.0.into_iter().map(|pair| pair.0).collect()); + // Pair lengths are checked after the complete WebIDL conversion. + let mut pairs = Vec::with_capacity(sequence.0.len()); + for pair in sequence.0 { + let Ok([key, value]) = <[_; 2]>::try_from(pair.0) else { + webidl::throw_error( + scope, + &webidl::WebIdlError::custom_message( + "URLSearchParams sequence pairs must contain exactly two items", + ), + ); + return None; + }; + pairs.push((key.0, value.0)); + } + return Some(pairs); } return record_string_pairs(scope, object.into()); } diff --git a/moli-renderer-v8/src/network_host/headers/store/init.rs b/moli-renderer-v8/src/network_host/headers/store/init.rs index 43028e2a66..cf09af3b51 100644 --- a/moli-renderer-v8/src/network_host/headers/store/init.rs +++ b/moli-renderer-v8/src/network_host/headers/store/init.rs @@ -20,7 +20,28 @@ pub(in crate::network_host) fn headers_entries_from_init<'s>( )); }; - if let Some(entries) = headers_entries_from_iterable_init(scope, init_arg, init_obj)? { + // Convert every inner sequence before validating or normalizing any header. + if let Some(sequence) = webidl::convert_optional_sequence::>( + scope, + init_arg, + webidl::Context::argument("Headers", 1), + &webidl::StringOptions::default(), + )? { + let mut entries = Vec::with_capacity(sequence.0.len()); + for pair in sequence.0 { + let [key, value] = <[_; 2]>::try_from(pair.0).map_err(|_| { + webidl::WebIdlError::custom_message( + "Headers sequence initializer pairs must have length 2", + ) + })?; + let entry = normalized_header_entry_or_throw(scope, key.into(), value.into()) + .ok_or_else(|| { + webidl::WebIdlError::custom_message( + "Headers initializer contains an invalid header", + ) + })?; + entries.push(entry); + } return Ok(entries); } @@ -40,186 +61,3 @@ pub(in crate::network_host) fn headers_entries_from_init<'s>( } Ok(entries) } - -fn headers_entries_from_iterable_init<'s>( - scope: &mut v8::PinScope<'s, '_>, - init_arg: v8::Local<'s, v8::Value>, - init_obj: v8::Local<'s, v8::Object>, -) -> Result>, webidl::WebIdlError> { - let iterator_key = v8::Symbol::get_iterator(scope); - let Some(iterator_value) = webidl::symbol_property_result( - scope, - init_obj, - iterator_key, - webidl::Context::member("Headers", "@@iterator"), - )? - else { - return Ok(None); - }; - if iterator_value.is_null_or_undefined() { - return Ok(None); - } - let Ok(iterator_method) = v8::Local::::try_from(iterator_value) else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer @@iterator must be callable", - )); - }; - let Some(iterator_value) = call_function_result( - scope, - iterator_method, - init_arg, - &[], - webidl::Context::member("Headers", "@@iterator"), - )? - else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer did not return an iterator", - )); - }; - let Ok(iterator) = v8::Local::::try_from(iterator_value) else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer did not return an iterator", - )); - }; - let Some(next_method) = webidl::property_result( - scope, - iterator, - "next", - webidl::Context::member("Headers", "next"), - )? - else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer iterator must have next()", - )); - }; - let Ok(next_method) = v8::Local::::try_from(next_method) else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer iterator must have next()", - )); - }; - - let mut entries = Vec::new(); - loop { - let Some(step_value) = call_function_result( - scope, - next_method, - iterator.into(), - &[], - webidl::Context::member("Headers", "next"), - )? - else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer next() must return an object", - )); - }; - let Ok(step) = v8::Local::::try_from(step_value) else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer next() must return an object", - )); - }; - let done = webidl::property_result( - scope, - step, - "done", - webidl::Context::member("Headers", "done"), - )? - .is_some_and(|value| value.boolean_value(scope)); - if done { - break; - } - let Some(pair) = webidl::property_result( - scope, - step, - "value", - webidl::Context::member("Headers", "value"), - )? - else { - return Err(webidl::WebIdlError::custom_message( - "Headers iterable initializer could not read an entry", - )); - }; - entries.push(header_sequence_pair_from_value(scope, pair)?); - } - Ok(Some(entries)) -} - -fn header_sequence_pair_from_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - pair_val: v8::Local<'s, v8::Value>, -) -> Result<(String, String), webidl::WebIdlError> { - let Ok(pair) = v8::Local::::try_from(pair_val) else { - return Err(webidl::WebIdlError::custom_message( - "Headers sequence initializer must contain pairs", - )); - }; - if webidl::property_result( - scope, - pair, - "length", - webidl::Context::member("Headers", "length"), - )? - .and_then(|value| value.uint32_value(scope)) - != Some(2) - { - return Err(webidl::WebIdlError::custom_message( - "Headers sequence initializer pairs must have length 2", - )); - } - let key_value = get_index_result(scope, pair, 0)?; - let key = header_init_byte_string(scope, key_value, "name")?; - let value_value = get_index_result(scope, pair, 1)?; - let value = header_init_byte_string(scope, value_value, "value")?; - normalized_header_entry_or_throw(scope, key, value).ok_or_else(|| { - webidl::WebIdlError::custom_message("Headers initializer contains an invalid header") - }) -} - -fn header_init_byte_string<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: Option>, - member: &'static str, -) -> Result { - let value = value.ok_or_else(|| { - webidl::WebIdlError::custom_message("Headers initializer could not read a value") - })?; - webidl::convert::(scope, value, webidl::Context::member("Headers", member)) - .map(Into::into) -} - -fn get_index_result<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - index: u32, -) -> Result>, webidl::WebIdlError> { - let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); - let mut scope = try_catch.init(); - match object.get_index(&scope, index) { - Some(value) => Ok(Some(value)), - None if scope.has_caught() => { - let _ = scope.rethrow(); - Err(webidl::WebIdlError::pending_exception( - webidl::Context::member("Headers", "sequence item"), - )) - } - None => Ok(None), - } -} - -fn call_function_result<'s>( - scope: &mut v8::PinScope<'s, '_>, - function: v8::Local<'s, v8::Function>, - receiver: v8::Local<'s, v8::Value>, - args: &[v8::Local<'s, v8::Value>], - context: webidl::Context, -) -> Result>, webidl::WebIdlError> { - let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); - let mut scope = try_catch.init(); - match function.call(&scope, receiver, args) { - Some(value) => Ok(Some(value)), - None if scope.has_caught() => { - let _ = scope.rethrow(); - Err(webidl::WebIdlError::pending_exception(context)) - } - None => Ok(None), - } -} diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs index 4bdc6c7e79..89756e904b 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -6108,7 +6108,7 @@ fn webidl_sequence_conversion_uses_iterator_without_mutable_array_from() { assert_eq!( result, - "arrayFrom:0:0.25,0.75|arrayIterator:0:throw:RangeError:array-iterator-used:1|order:iterator,next:0,done:0,value:0,toString:0,next:1,done:1,value:1,toString:1,next:2,done:2|nextThrow:throw:RangeError:next-boom|doneThrow:throw:TypeError:done-boom|valueThrow:throw:SyntaxError:value-boom|elementThrow:throw:URIError:string-boom:true" + "arrayFrom:0:0.25,0.75|arrayIterator:0:throw:RangeError:array-iterator-used:1|order:iterator,next:0,done:0,value:0,toString:0,next:1,done:1,value:1,toString:1,next:2,done:2|nextThrow:throw:RangeError:next-boom|doneThrow:throw:TypeError:done-boom|valueThrow:throw:SyntaxError:value-boom|elementThrow:throw:URIError:string-boom:false" ); } @@ -6152,8 +6152,13 @@ fn url_search_params_sequence_discrimination_reads_iterator_once() { try { new URLSearchParams({ [Symbol.iterator]() { + let done = false; return { - next() { return { done: false, value: ['short'] }; }, + next() { + if (done) return { done: true }; + done = true; + return { done: false, value: ['short'] }; + }, return() { outerClosed = true; throw new SyntaxError('close error'); @@ -6204,7 +6209,10 @@ fn url_search_params_sequence_discrimination_reads_iterator_once() { ) .expect("URLSearchParams sequence discrimination probe should evaluate"); - assert_eq!(result, "1|one|two|TypeError|TypeError:true|RangeError:true"); + assert_eq!( + result, + "1|one|two|TypeError|TypeError:false|RangeError:false" + ); } #[test] @@ -9859,3 +9867,238 @@ fn fetch_body_json_removes_initial_utf8_bom_and_preserves_syntax_errors() { fn fetch_body_binary_and_form_methods_preserve_bom_bytes() { assert_body_utf8_bom_probe("bytes"); } + +#[test] +fn webidl_sequences_propagate_abrupt_completion_without_closing_iterators() { + let mut vm = new_storage_test_vm("https://sequence-abrupt.test/"); + let result = vm.eval(r#" +(() => { + const check = (condition, label) => { if (!condition) throw new Error(label); }; + const consumers = [ + ['USVString', input => new URLSearchParams([input])], + ['DOMString', input => new PerformanceObserver(() => {}).observe({entryTypes: input})] + ]; + if (typeof IntersectionObserver === 'function') consumers.push( + ['double', input => new IntersectionObserver(() => {}, {threshold: input})]); + for (const [name, consume] of consumers) { + for (const stage of ['next', 'done', 'value', 'convert', 'symbol']) { + const marker = {}; + const log = []; + const fail = () => { log.push(stage); throw marker; }; + const input = {[Symbol.iterator]() { + let finished = false; + return { + next() { + if (finished) return {done: true}; + finished = true; + if (stage === 'next') fail(); + return { + get done() { if (stage === 'done') fail(); return false; }, + get value() { + if (stage === 'value') fail(); + return stage === 'symbol' ? Symbol() : {[Symbol.toPrimitive]: fail}; + } + }; + }, + get return() { log.push('get:return'); throw new Error('return must not be read'); } + }; + }}; + let caught; + try { consume(input); } catch (error) { caught = error; } + check(stage === 'symbol' ? caught instanceof TypeError : caught === marker, name + ' ' + stage + ' exception'); + const expected = stage === 'symbol' ? [] : [stage]; + check(JSON.stringify(log) === JSON.stringify(expected), name + ' ' + stage + ': ' + JSON.stringify(log)); + } + } + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + +#[test] +fn webidl_nested_and_interface_sequences_do_not_read_iterator_return_on_errors() { + let mut vm = new_storage_test_vm("https://nested-sequence-abrupt.test/"); + let result = vm.eval(r#" +(() => { + const check = (condition, label) => { if (!condition) throw new Error(label); }; + for (const stage of ['iterator', 'next', 'value', 'convert']) { + const marker = {}; + let returnReads = 0; + const fail = () => { throw marker; }; + const wrap = value => ({[Symbol.iterator]() { + let finished = false; + return { + next() { + if (finished) return {done: true}; + finished = true; + return {done: false, value}; + }, + get return() { returnReads++; throw new Error('outer return'); } + }; + }}); + const pair = {get [Symbol.iterator]() { + if (stage === 'iterator') fail(); + return function() { + let finished = false; + return { + next() { + if (finished) return {done: true}; + finished = true; + if (stage === 'next') fail(); + return {done: false, get value() { + if (stage === 'value') fail(); + return {[Symbol.toPrimitive]: fail}; + }}; + }, + get return() { returnReads++; throw new Error('inner return'); } + }; + }; + }}; + let caught; + try { new URLSearchParams(wrap(pair)); } catch (error) { caught = error; } + check(caught === marker && returnReads === 0, stage + ' must propagate without closing either iterator'); + } + for (const member of ['coalescedEvents', 'predictedEvents']) { + let returnReads = 0; + const events = {[Symbol.iterator]() { + let finished = false; + return { + next() { + if (finished) return {done: true}; + finished = true; + return {done: false, value: new Event('invalid')}; + }, + get return() { returnReads++; throw new Error('interface sequence return'); } + }; + }}; + let caught; + try { new PointerEvent('pointermove', {[member]: events}); } catch (error) { caught = error; } + check(caught instanceof TypeError && returnReads === 0, member + ' must reject the interface without closing'); + } + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + +#[test] +fn initializer_sequences_convert_all_entries_before_validating_pairs() { + let mut vm = new_storage_test_vm("https://initializer-sequence.test/"); + let result = vm.eval(r#" +(() => { + const check = (condition, label) => { if (!condition) throw new Error(label); }; + const consumers = [ + ['Headers', input => new Headers(input)], + ['URLSearchParams', input => new URLSearchParams(input)], + ['Request', input => new Request('https://initializer-sequence.test/', {headers: input})], + ['Response', input => new Response(null, {headers: input})] + ]; + for (const [name, consume] of consumers) { + const invalidPairs = [['short'], ['key', 'value', 'extra']]; + if (name !== 'URLSearchParams') invalidPairs.push(['', 'value'], ['key', 'bad\nvalue']); + for (const bad of invalidPairs) { + for (const stage of ['complete', 'next', 'done', 'value', 'convert']) { + const marker = {}; + const log = []; + const fail = () => { log.push('fail:' + stage); throw marker; }; + const input = {[Symbol.iterator]() { + let index = 0; + return { + next() { + log.push('next:' + index); + if (index++ === 0) return {done: false, value: bad}; + if (index === 2) { + if (stage === 'next') fail(); + return { + get done() { if (stage === 'done') fail(); return false; }, + get value() { + if (stage === 'value') fail(); + return ['later', {toString() { + if (stage === 'convert') fail(); + log.push('convert:later'); + return 'value'; + }}]; + } + }; + } + return {done: true}; + }, + get return() { log.push('return'); throw new Error('return must not be read'); } + }; + }}; + let caught; + try { consume(input); } catch (error) { caught = error; } + const label = name + ' ' + JSON.stringify(bad) + ' ' + stage; + check(stage === 'complete' ? caught instanceof TypeError : caught === marker, label + ' exception'); + const expected = stage === 'complete' ? ['next:0', 'next:1', 'convert:later', 'next:2'] : + ['next:0', 'next:1', 'fail:' + stage]; + check(JSON.stringify(log) === JSON.stringify(expected), label + ': ' + JSON.stringify(log)); + } + } + } + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + +#[test] +fn initializer_pairs_use_inner_iterators_and_convert_extra_elements() { + let mut vm = new_storage_test_vm("https://initializer-inner-sequence.test/"); + let result = vm.eval(r#" +(() => { + const check = (condition, label) => { if (!condition) throw new Error(label); }; + for (const Constructor of [Headers, URLSearchParams]) { + let iteratorReads = 0; + const pair = { + get length() { throw new Error('length must not be read'); }, + get 0() { throw new Error('indexed properties must not be read'); }, + get [Symbol.iterator]() { + iteratorReads++; + return function*() { + check(this === pair, 'inner iterator receiver'); + yield 'X-Key'; + yield ' one '; + }; + } + }; + const expected = Constructor === Headers ? [['x-key', 'one']] : [['X-Key', ' one ']]; + const actual = Array.from(new Constructor([pair])); + check(JSON.stringify(actual) === JSON.stringify(expected) && iteratorReads === 1, Constructor.name + ' inner iteration'); + for (const invalid of ['ab', {0: 'key', 1: 'value', length: 2}]) { + let caught; + try { new Constructor([invalid]); } catch (error) { caught = error; } + check(caught instanceof TypeError, Constructor.name + ' requires object iterables'); + } + const marker = {}; + let conversions = 0; + let caught; + try { + new Constructor([['key', 'value', {toString() { conversions++; throw marker; }}]]); + } catch (error) { caught = error; } + check(caught === marker && conversions === 1, Constructor.name + ' must convert the extra element'); + + const log = []; + const input = {[Symbol.iterator]() { + let index = 0; + return {next() { + log.push('next:' + index); + if (index++ > 0) return {done: true}; + return {done: false, value: ['key', 'value', {toString() { + log.push('extra'); + return '\u0100'; + }}]}; + }}; + }}; + caught = undefined; + try { new Constructor(input); } catch (error) { caught = error; } + check(caught instanceof TypeError, Constructor.name + ' rejects the invalid initializer'); + const expectedLog = Constructor === Headers ? ['next:0', 'extra'] : ['next:0', 'extra', 'next:1']; + check(JSON.stringify(log) === JSON.stringify(expectedLog), Constructor.name + ': ' + JSON.stringify(log)); + } + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} diff --git a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs index 5d6ccb6b6d..d6c8dbd3d4 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -239,11 +239,6 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[ 1, DirectCallOwner::NativeForwardingOrScript, ), - allowed( - "network_host/headers/store/init.rs", - 1, - DirectCallOwner::NativeForwardingOrScript, - ), allowed( "script_vm/frame_script_jobs.rs", 1, diff --git a/moli-webidl/src/convert.rs b/moli-webidl/src/convert.rs index 41afa68e46..a07af9bced 100644 --- a/moli-webidl/src/convert.rs +++ b/moli-webidl/src/convert.rs @@ -352,28 +352,8 @@ where sequence_iterator_from_method(scope, value, iterator_method, context)?; let mut values = Vec::new(); while let Some(item) = sequence_iterator_next(scope, iterator, next_method, context)? { - let (converted, caught_exception) = { - let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); - let mut conversion_scope = try_catch.init(); - let converted = T::convert(&mut conversion_scope, item, context, options); - let caught_exception = conversion_scope - .has_caught() - .then(|| conversion_scope.exception()) - .flatten() - .map(|exception| v8::Global::new(&conversion_scope, exception)); - (converted, caught_exception) - }; - match converted { - Ok(value) => values.push(value), - Err(error) => { - sequence_iterator_close_ignoring_errors(scope, iterator); - if let Some(exception) = caught_exception { - let exception = v8::Local::new(scope, &exception); - scope.throw_exception(exception); - } - return Err(error); - } - } + // WebIDL sequence conversion propagates errors without IteratorClose. + values.push(T::convert(scope, item, context, options)?); } Ok(Some(Sequence(values))) } @@ -1157,27 +1137,6 @@ fn sequence_iterator_next<'s>( Ok(Some(value)) } -fn sequence_iterator_close_ignoring_errors<'s>( - scope: &mut v8::PinScope<'s, '_>, - iterator: v8::Local<'s, v8::Object>, -) { - let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); - let scope = try_catch.init(); - let Some(return_key) = v8::String::new(&scope, "return") else { - return; - }; - let Some(return_method) = iterator.get(&scope, return_key.into()) else { - return; - }; - if return_method.is_null_or_undefined() { - return; - } - let Ok(return_method) = v8::Local::::try_from(return_method) else { - return; - }; - let _ = return_method.call(&scope, iterator.into(), &[]); -} - fn call_sequence_function<'s>( scope: &mut v8::PinScope<'s, '_>, function: v8::Local<'s, v8::Function>, diff --git a/moli-wpt-compat/fixtures/wpt/ported/url/urlsearchparams-basic.html b/moli-wpt-compat/fixtures/wpt/ported/url/urlsearchparams-basic.html index 2bd31cbbe5..fde2ecc822 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/url/urlsearchparams-basic.html +++ b/moli-wpt-compat/fixtures/wpt/ported/url/urlsearchparams-basic.html @@ -431,9 +431,9 @@ test(function () { }, }); }, "top-level iterator should propagate pair conversion errors"); - assert_true( + assert_false( closedAfterStringification, - "pair conversion errors should close the top-level iterator", + "pair conversion errors must not close the top-level iterator", ); let closedAfterInvalidPair = false; @@ -444,10 +444,8 @@ test(function () { return { next: function () { count += 1; - return { - done: false, - value: count === 1 ? ["short"] : ["unreached", "value"], - }; + if (count === 1) return { done: false, value: ["short"] }; + return { done: true }; }, return: function () { closedAfterInvalidPair = true; @@ -457,11 +455,11 @@ test(function () { }, }); }, "top-level iterator should propagate invalid pair errors"); - assert_true( + assert_false( closedAfterInvalidPair, - "invalid pair errors should close the top-level iterator", + "invalid pair errors must not close the top-level iterator", ); -}, "URLSearchParams constructor closes top-level iterators on pair conversion errors"); +}, "URLSearchParams constructor does not close top-level iterators on pair errors"); test(function () { let innerClosedAfterValueConversion = false; @@ -497,11 +495,11 @@ test(function () { assert_throws_name("RangeError", function () { new URLSearchParams([pair]); }, "inner pair iterator should propagate element conversion errors"); - assert_true( + assert_false( innerClosedAfterValueConversion, - "pair element conversion errors should close the inner pair iterator", + "pair element conversion errors must not close the inner pair iterator", ); -}, "URLSearchParams constructor closes inner pair iterators on element conversion errors"); +}, "URLSearchParams constructor does not close inner pair iterators on conversion errors"); test(function () { let topLevelReturnCalled = false; @@ -527,7 +525,7 @@ test(function () { }, }); }, "top-level iterator return errors should not replace the original conversion error"); - assert_true(topLevelReturnCalled, "top-level iterator return should be called"); + assert_false(topLevelReturnCalled, "top-level iterator return must not be called"); let innerReturnCalled = false; const pair = { @@ -558,16 +556,19 @@ test(function () { assert_throws_name("RangeError", function () { new URLSearchParams([pair]); }, "inner pair iterator return errors should not replace the original conversion error"); - assert_true(innerReturnCalled, "inner pair iterator return should be called"); -}, "URLSearchParams constructor preserves original errors when iterator return throws"); + assert_false(innerReturnCalled, "inner pair iterator return must not be called"); +}, "URLSearchParams constructor does not call throwing return methods after conversion errors"); test(function () { let returnCalled = false; assert_throws_name("TypeError", function () { new URLSearchParams({ [Symbol.iterator]: function () { + let done = false; return { next: function () { + if (done) return { done: true }; + done = true; return { done: false, value: ["short"] }; }, return: function () { @@ -578,8 +579,8 @@ test(function () { }, }); }, "top-level iterator return errors should not replace invalid pair length errors"); - assert_true(returnCalled, "top-level iterator return should be called"); -}, "URLSearchParams constructor preserves invalid pair length errors when iterator return throws"); + assert_false(returnCalled, "top-level iterator return must not be called"); +}, "URLSearchParams constructor does not call throwing return methods for invalid pairs"); test(function () { let topLevelReturnCalled = false;