From 2a3aa78f8d3cdaf3a6a9b397e55052471fa006ef Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 16 Sep 2026 02:10:35 +0800 Subject: [PATCH] fix(fetch): inherit Headers methods and observe initializer iterators Use the intrinsic Headers prototype for internally created header objects. Remove own method copies and platform-object initializer shortcuts so author iterator overrides, exceptions and record properties remain observable. Source: 66434f2877494de4b33222aeeb84297609482419, c229deb38c97a6546a0950a662f46b1cabf316a0 --- .../context_bootstrap/form_data_runtime.rs | 2 +- .../src/context_bootstrap/url_form.rs | 5 +- .../src/context_bootstrap/url_form/helpers.rs | 15 -- .../url_search_params_runtime.rs | 5 +- .../url_search_params_runtime/parse.rs | 25 -- moli-renderer-v8/src/network_host/headers.rs | 1 - .../src/network_host/headers/methods.rs | 38 +-- .../src/network_host/headers/store/init.rs | 6 +- .../network_host/headers/store/projection.rs | 2 +- moli-renderer-v8/src/network_host/request.rs | 1 - .../src/network_host/request/bindings.rs | 1 - moli-renderer-v8/src/network_host/response.rs | 2 +- .../network_host/response/bindings/init.rs | 1 - .../src/network_host/response/materialize.rs | 3 - .../src/script_vm/tests/webidl_fetch.rs | 249 +++++++++++++++++- 15 files changed, 252 insertions(+), 104 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs b/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs index e43fba0a44..23443c310d 100644 --- a/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs @@ -15,7 +15,7 @@ pub(crate) use request_body::{ form_data_entries_multipart_body_with_prefix, form_data_request_body, }; pub(crate) use serialize::form_data_entries_to_string_pairs; -pub(in crate::context_bootstrap) use storage::{form_data_entries, form_data_is_object}; +pub(in crate::context_bootstrap) use storage::form_data_is_object; pub(super) use template::build_form_data_constructor_template; pub(crate) fn form_data_object_from_entries<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/url_form.rs b/moli-renderer-v8/src/context_bootstrap/url_form.rs index c9fb922a76..e34d86f9d1 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_form.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_form.rs @@ -6,8 +6,5 @@ mod callbacks; mod helpers; mod template; -pub(super) use helpers::{ - apply_url_update, callback_arg_url_like_string, callback_value_string, url_href_slot, - url_object_value, -}; +pub(super) use helpers::{apply_url_update, callback_value_string, url_object_value}; pub(super) use template::build_url_constructor_template; diff --git a/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs b/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs index 7298cf81f9..23e8ad5ecb 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs @@ -11,21 +11,6 @@ pub(in crate::context_bootstrap) fn callback_value_string( .map(|value| value.to_rust_string_lossy(scope)) } -pub(in crate::context_bootstrap) fn callback_arg_url_like_string<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> Option { - if value.is_null_or_undefined() { - return None; - } - if let Ok(object) = v8::Local::::try_from(value) - && let Some(href) = url_href_slot(scope, object) - { - return Some(href); - } - callback_value_string(scope, value) -} - pub(super) fn resolve_url_constructor_input( input: &str, base: Option<&str>, diff --git a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs index a719bf1fde..82f9a469b4 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs @@ -1,7 +1,4 @@ -use super::form_data_runtime::{form_data_entries, form_data_is_object}; -use super::url_form::{ - apply_url_update, callback_arg_url_like_string, callback_value_string, url_object_value, -}; +use super::url_form::{apply_url_update, callback_value_string, url_object_value}; use super::*; use crate::util::{get_private_object, get_private_value, set_private_value}; use crate::web_api_interfaces; 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 963c30a751..2ff21e0b27 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,6 +1,3 @@ -use super::storage::{url_search_params_is_object, url_search_params_pairs}; -use super::*; -use crate::context_bootstrap::url_form::url_href_slot; use crate::webidl; use moli_url::search_params::{SearchParamPair, parse_search_params}; @@ -38,17 +35,6 @@ pub(super) fn url_search_params_pairs_from_constructor<'s>( return Some(Vec::new()); } if let Ok(object) = v8::Local::::try_from(value) { - if form_data_is_object(scope, object) { - return Some( - form_data_entries(scope, object) - .into_iter() - .filter_map(|(key, value)| { - callback_value_string(scope, v8::Local::new(scope, &value)) - .map(|value| (key, value)) - }) - .collect(), - ); - } let sequence = match webidl::convert_optional_sequence::( scope, value, @@ -64,17 +50,6 @@ 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()); } - if url_search_params_is_object(scope, object) { - return Some(url_search_params_pairs(scope, object)); - } - if url_href_slot(scope, object).is_some() { - return Some( - callback_arg_url_like_string(scope, value) - .as_deref() - .map(parse_search_params) - .unwrap_or_default(), - ); - } return record_string_pairs(scope, object.into()); } Some( diff --git a/moli-renderer-v8/src/network_host/headers.rs b/moli-renderer-v8/src/network_host/headers.rs index 4fbecf055b..33273f271e 100644 --- a/moli-renderer-v8/src/network_host/headers.rs +++ b/moli-renderer-v8/src/network_host/headers.rs @@ -5,7 +5,6 @@ mod store; use super::*; pub(crate) use self::bindings::headers_constructor_callback; -pub(super) use self::methods::install_headers_object_methods; pub(crate) use self::methods::install_headers_template_bindings; pub(crate) use self::store::headers_entries; pub(crate) use self::store::{HeadersGuard, filter_headers_for_guard}; diff --git a/moli-renderer-v8/src/network_host/headers/methods.rs b/moli-renderer-v8/src/network_host/headers/methods.rs index 2557eaf6fc..4e7a801a57 100644 --- a/moli-renderer-v8/src/network_host/headers/methods.rs +++ b/moli-renderer-v8/src/network_host/headers/methods.rs @@ -15,7 +15,7 @@ pub(super) use self::iteration::{ pub(super) use self::mutation::{ headers_append_callback, headers_delete_callback, headers_set_callback, }; -use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; +use moli_webapi_declare::WebApiFunctionTemplate; pub(super) fn require_headers_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -28,33 +28,6 @@ pub(super) fn require_headers_receiver<'s>( None } -#[derive(Default, WebApiObject)] -#[webapi(interface = web_api_interfaces::Headers)] -struct HeadersObjectMethodsDeclaration { - #[webapi(method, enumerable, length = 1, callback = headers_get_callback)] - get: (), - #[webapi(method, enumerable, length = 1, callback = headers_has_callback)] - has: (), - #[webapi(method, enumerable, length = 0, callback = headers_get_set_cookie_callback)] - get_set_cookie: (), - #[webapi(method, enumerable, length = 2, callback = headers_set_callback)] - set: (), - #[webapi(method, enumerable, length = 1, callback = headers_delete_callback)] - delete: (), - #[webapi(method, enumerable, length = 2, callback = headers_append_callback)] - append: (), - #[webapi(method, enumerable, length = 0, callback = headers_keys_callback)] - keys: (), - #[webapi(method, enumerable, length = 0, callback = headers_values_callback)] - values: (), - #[webapi(method, enumerable, length = 0, callback = headers_entries_callback)] - entries: (), - #[webapi(alias = "entries", symbol = "iterator", enumerable)] - iterator: (), - #[webapi(method, enumerable, length = 1, callback = headers_for_each_callback)] - for_each: (), -} - #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::Headers, enumerable)] struct HeadersPrototypeMethodsDeclaration { @@ -82,15 +55,6 @@ struct HeadersPrototypeMethodsDeclaration { for_each: (), } -pub(in crate::network_host) fn install_headers_object_methods<'s>( - scope: &mut v8::PinScope<'s, '_>, - headers_obj: v8::Local<'s, v8::Object>, -) { - HeadersObjectMethodsDeclaration::default() - .initialize(scope, headers_obj) - .expect("Headers object methods declaration should initialize"); -} - pub(crate) fn install_headers_template_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, 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 43ac8c3724..43028e2a66 100644 --- a/moli-renderer-v8/src/network_host/headers/store/init.rs +++ b/moli-renderer-v8/src/network_host/headers/store/init.rs @@ -1,4 +1,4 @@ -use super::entries::{headers_entries_if_present, normalized_header_entry_or_throw}; +use super::entries::normalized_header_entry_or_throw; use crate::webidl; pub(in crate::network_host) fn headers_entries_from_init<'s>( @@ -24,10 +24,6 @@ pub(in crate::network_host) fn headers_entries_from_init<'s>( return Ok(entries); } - if let Some(entries) = headers_entries_if_present(scope, init_obj) { - return Ok(entries); - } - let record = webidl::convert::>( scope, init_obj.into(), diff --git a/moli-renderer-v8/src/network_host/headers/store/projection.rs b/moli-renderer-v8/src/network_host/headers/store/projection.rs index 511b5ec5df..b855f54902 100644 --- a/moli-renderer-v8/src/network_host/headers/store/projection.rs +++ b/moli-renderer-v8/src/network_host/headers/store/projection.rs @@ -7,7 +7,7 @@ use crate::web_api_interfaces; use moli_webapi_declare::WebApiObject; #[derive(WebApiObject)] -#[webapi(interface = web_api_interfaces::Headers, prototype = "Object")] +#[webapi(interface = web_api_interfaces::Headers)] struct HeadersStorageDeclaration { #[webapi(slot = HEADERS_ENTRIES_SLOT)] entries: String, diff --git a/moli-renderer-v8/src/network_host/request.rs b/moli-renderer-v8/src/network_host/request.rs index dec938d22a..5d69b7d752 100644 --- a/moli-renderer-v8/src/network_host/request.rs +++ b/moli-renderer-v8/src/network_host/request.rs @@ -4,7 +4,6 @@ mod input; use super::headers::{ build_headers_object_with_state, filter_headers_for_guard, headers_entries_from_init, - install_headers_object_methods, }; use super::*; diff --git a/moli-renderer-v8/src/network_host/request/bindings.rs b/moli-renderer-v8/src/network_host/request/bindings.rs index 4560bbc30d..e46e6b2daa 100644 --- a/moli-renderer-v8/src/network_host/request/bindings.rs +++ b/moli-renderer-v8/src/network_host/request/bindings.rs @@ -102,7 +102,6 @@ pub(crate) fn request_constructor_callback<'s>( request_headers_guard_for_mode(&state.mode), false, ); - install_headers_object_methods(scope, headers_obj); let body_value = body_buffer .and_then(|buffer| { diff --git a/moli-renderer-v8/src/network_host/response.rs b/moli-renderer-v8/src/network_host/response.rs index 98a74a3194..300d9d5874 100644 --- a/moli-renderer-v8/src/network_host/response.rs +++ b/moli-renderer-v8/src/network_host/response.rs @@ -5,7 +5,7 @@ mod materialize; use super::headers::{ HeadersGuard, build_headers_object_with_state, filter_headers_for_guard, headers_entries, - headers_entries_from_init, install_headers_object_methods, + headers_entries_from_init, }; use super::*; diff --git a/moli-renderer-v8/src/network_host/response/bindings/init.rs b/moli-renderer-v8/src/network_host/response/bindings/init.rs index afd178f9d5..a85baf229f 100644 --- a/moli-renderer-v8/src/network_host/response/bindings/init.rs +++ b/moli-renderer-v8/src/network_host/response/bindings/init.rs @@ -80,7 +80,6 @@ pub(super) fn install_response_headers( let entries = filter_headers_for_guard(&entries, HeadersGuard::Response); let headers_obj = build_headers_object_with_state(scope, &entries, HeadersGuard::Response, false); - install_headers_object_methods(scope, headers_obj); set_response_slot_value(scope, obj, RESPONSE_HEADERS_SLOT, headers_obj.into()); } diff --git a/moli-renderer-v8/src/network_host/response/materialize.rs b/moli-renderer-v8/src/network_host/response/materialize.rs index 2f504ca915..0c13c90aa5 100644 --- a/moli-renderer-v8/src/network_host/response/materialize.rs +++ b/moli-renderer-v8/src/network_host/response/materialize.rs @@ -366,7 +366,6 @@ fn build_fetch_response_object_head<'s>( let internal_headers = filter_headers_for_guard(&head.headers, HeadersGuard::Response); let internal_headers_obj = build_headers_object_with_state(scope, &internal_headers, HeadersGuard::Response, true); - install_headers_object_methods(scope, internal_headers_obj); set_response_slot_value( scope, obj, @@ -399,7 +398,6 @@ fn finish_fetch_response_object_with_body_stream<'s>( let headers = filter_headers_for_guard(header_entries, HeadersGuard::Response); let headers_obj = build_headers_object_with_state(scope, &headers, HeadersGuard::Response, true); - install_headers_object_methods(scope, headers_obj); set_response_slot_value(scope, obj, RESPONSE_HEADERS_SLOT, headers_obj.into()); let body_value = if !filtered_response_exposes_body(filter) { @@ -442,7 +440,6 @@ pub(crate) fn build_filtered_cached_response_object<'s>( let headers = filter_headers_for_guard(&[], HeadersGuard::Response); let headers_obj = build_headers_object_with_state(scope, &headers, HeadersGuard::Response, true); - install_headers_object_methods(scope, headers_obj); set_response_slot_value(scope, obj, RESPONSE_HEADERS_SLOT, headers_obj.into()); set_filtered_response_internal_body_from_bytes(scope, obj, body); FetchResponseBodyDeclaration::new(String::new(), v8::null(scope).into()) 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 56aa76811f..4bdc6c7e79 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -6702,6 +6702,133 @@ fn vtt_cue_constructor_applies_webidl_conversion() { ); } +#[test] +fn webidl_initializer_unions_observe_iterators_on_platform_objects() { + let mut vm = new_storage_test_vm("https://initializer-union.test/"); + let result = vm.eval(r#" +(() => { + const check = (condition, label) => { if (!condition) throw new Error(label); }; + const equal = (actual, expected, label) => check( + JSON.stringify(actual) === JSON.stringify(expected), label + ': ' + JSON.stringify(actual)); + const factories = [ + ['Headers', () => new Headers([['x-original', 'original']])], + ['URLSearchParams', () => new URLSearchParams([['x-original', 'original']])], + ['FormData', () => { + const form = new FormData(); + form.append('x-original', 'original'); + return form; + }] + ]; + const consumers = [ + ['Headers', input => new Headers(input)], + ['URLSearchParams', input => new URLSearchParams(input)], + ['Request', input => new Request('https://initializer-union.test/', {headers: input}).headers], + ['Response', input => new Response(null, {headers: input}).headers] + ]; + for (const [target, create] of consumers) { + for (const [source, factory] of factories) { + for (const mode of ['original', 'custom', 'undefined', 'null', 'throw', 'noncallable']) { + const label = target + ' from ' + source + ' with ' + mode + ' iterator'; + const input = factory(); + const original = input[Symbol.iterator]; + const marker = {}; + const log = []; + Object.defineProperty(input, 'x-record', {enumerable: true, get() { + log.push('record'); + return 'record'; + }}); + Object.defineProperty(input, Symbol.iterator, {get() { + log.push('iterator'); + if (mode === 'throw') throw marker; + if (mode === 'noncallable') return 42; + if (mode === 'undefined') return undefined; + if (mode === 'null') return null; + if (mode === 'original') return original; + return function*() { + check(this === input, label + ' receiver'); + log.push('iterate'); + yield ['x-custom', 'custom']; + }; + }}); + if (mode === 'throw' || mode === 'noncallable') { + let caught; + try { create(input); } catch (error) { caught = error; } + check(mode === 'throw' ? caught === marker : caught instanceof TypeError, label + ' exception'); + } else { + const expected = mode === 'original' ? [['x-original', 'original']] : + mode === 'custom' ? [['x-custom', 'custom']] : [['x-record', 'record']]; + equal(Array.from(create(input)), expected, label); + } + const expectedLog = ['iterator']; + if (mode === 'custom') expectedLog.push('iterate'); + if (mode === 'undefined' || mode === 'null') expectedLog.push('record'); + equal(log, expectedLog, label + ' conversion order'); + } + } + } + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + +#[test] +fn webidl_initializer_unions_convert_url_objects_as_records() { + let mut vm = new_storage_test_vm("https://initializer-record.test/"); + let result = vm + .eval( + r#" +(() => { + for (const Constructor of [Headers, URLSearchParams]) { + const url = new URL('https://initializer-record.test/path?query=value'); + Object.defineProperty(url, Symbol.toPrimitive, {value() { + throw new Error('URL object must use record conversion'); + }}); + if (Array.from(new Constructor(url)).length !== 0) throw new Error('empty URL record'); + url['x-record'] = 'record'; + if (JSON.stringify(Array.from(new Constructor(url))) !== '[["x-record","record"]]') { + throw new Error(Constructor.name + ' must use own URL properties'); + } + } + return 'ok'; +})() +"#, + ) + .unwrap(); + assert_eq!(result, "ok"); +} + +#[test] +fn url_search_params_form_data_initializers_convert_file_values_through_the_iterator() { + let mut vm = new_storage_test_vm("https://form-data-initializer.test/"); + let result = vm.eval(r#" +(() => { + const file = new File(['contents'], 'entry.txt'); + const form = new FormData(); + form.append('a', 'first'); + form.append('file', file); + form.append('a', 'second'); + const params = new URLSearchParams(form); + form.append('later', 'ignored'); + if (JSON.stringify(Array.from(params)) !== '[["a","first"],["file","[object File]"],["a","second"]]') { + throw new Error('FormData entries must be copied in iteration order with File stringification'); + } + const marker = {}; + let conversions = 0; + Object.defineProperty(file, Symbol.toPrimitive, {value(hint) { + if (hint !== 'string') throw new Error('USVString must use the string hint'); + conversions++; + throw marker; + }}); + let caught; + try { new URLSearchParams(form); } catch (error) { caught = error; } + if (caught !== marker || conversions !== 1) throw new Error('File conversion exception must propagate'); + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + #[test] fn headers_methods_parse_webidl_arguments() { let mut vm = new_storage_test_vm("https://headers-methods.test/"); @@ -7029,6 +7156,117 @@ async fn form_data_and_url_search_params_for_each_use_callback_relevant_realm() ); } +#[test] +fn request_and_response_headers_share_intrinsic_prototype_methods() { + let mut vm = new_storage_test_vm("https://headers-object-prototype.test/"); + let result = vm + .eval( + r#" +(() => { + const check = (value, label) => { if (!value) throw new Error(label); }; + const NativeHeaders = Headers; + const prototype = NativeHeaders.prototype; + const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'Headers'); + const nativeGet = prototype.get; + let lookups = 0; + Object.defineProperty(globalThis, 'Headers', {configurable: true, get() { + lookups++; + throw new Error('public Headers lookup'); + }}); + try { + const request = new Request('/request', {headers: {'x-test': 'request'}}); + const streamRequest = new Request('/stream', {method: 'POST', duplex: 'half', + body: new ReadableStream({start(controller) {controller.close();}})}); + const response = new Response('body', {headers: {'x-test': 'response'}}); + const error = Response.error(); + const redirect = Response.redirect('/target'); + const holders = [request, request.clone(), streamRequest, streamRequest.clone(), + response, response.clone(), error, error.clone(), redirect, redirect.clone()]; + const methods = ['get', 'has', 'getSetCookie', 'set', 'delete', 'append', + 'keys', 'values', 'entries', 'forEach', Symbol.iterator]; + for (const holder of holders) { + const headers = holder.headers; + check(headers === holder.headers, 'same Headers object'); + check(Object.getPrototypeOf(headers) === prototype && headers instanceof NativeHeaders, + 'intrinsic Headers prototype'); + check(Object.prototype.toString.call(headers) === '[object Headers]', 'Headers class'); + check(Reflect.ownKeys(headers).length === 0, 'no own methods'); + for (const key of methods) check(headers[key] === prototype[key], String(key)); + } + check(nativeGet.call(request.headers, 'x-test') === 'request', 'request entries'); + check(nativeGet.call(response.headers, 'x-test') === 'response', 'response entries'); + prototype.get = function() { return this; }; + for (const holder of holders) check(holder.headers.get() === holder.headers, + 'prototype method changes must remain observable'); + const fresh = new Response('fresh').headers; + check(fresh.get() === fresh, 'new objects share updated prototype methods'); + check(lookups === 0, 'factory must not consult replaced global'); + return 'ok'; + } finally { + prototype.get = nativeGet; + Object.defineProperty(globalThis, 'Headers', descriptor); + } +})() +"#, + ) + .unwrap(); + assert_eq!(result, "ok"); +} + +#[tokio::test] +async fn response_headers_keep_receiver_realm_across_borrowed_getters() { + let mut vm = new_storage_test_vm("https://headers-owner-realm.test/"); + vm.eval( + r#" +const root = document.documentElement || document.appendChild(document.createElement('html')); +const body = document.body || root.appendChild(document.createElement('body')); +globalThis.headersFrame = document.createElement('iframe'); +body.appendChild(headersFrame); +"#, + ) + .unwrap(); + assert_initial_about_blank_child_completed_synchronously_for_test(&mut vm, "Headers realm") + .await; + let _ = materialize_single_child_default_realm_for_test(&mut vm, "Headers realm"); + let result = vm.eval(r#" +(() => { + const check = (value, label) => { if (!value) throw new Error(label); }; + const child = headersFrame.contentWindow; + const parentHeaders = Headers; + const childHeaders = child.Headers; + const parentResponse = new Response('parent', {headers: {'x-realm': 'parent'}}); + const childResponse = new child.Response('child', {headers: {'x-realm': 'child'}}); + const parentGetter = Object.getOwnPropertyDescriptor(Response.prototype, 'headers').get; + const childGetter = Object.getOwnPropertyDescriptor(child.Response.prototype, 'headers').get; + const descriptors = [globalThis, child].map(realm => Object.getOwnPropertyDescriptor(realm, 'Headers')); + for (const realm of [globalThis, child]) Object.defineProperty(realm, 'Headers', { + configurable: true, get() { throw new Error('public Headers lookup'); } + }); + try { + for (const [response, ctor, getter, value] of [ + [parentResponse, parentHeaders, childGetter, 'parent'], + [childResponse, childHeaders, parentGetter, 'child'] + ]) { + check(getter.call(response) === response.headers, 'borrowed getter returns associated Headers'); + for (const entry of [response]) { + const headers = entry.headers; + check(Object.getPrototypeOf(headers) === ctor.prototype && headers instanceof ctor, + 'Headers must use the response realm'); + check(headers.get === ctor.prototype.get && Reflect.ownKeys(headers).length === 0, + 'Headers must share realm prototype methods'); + check(headers.get('x-realm') === value, 'cross-realm entries'); + check(ctor.prototype.get.call(headers, 'x-realm') === value, 'branded receiver'); + } + } + return 'ok'; + } finally { + [globalThis, child].forEach((realm, index) => Object.defineProperty(realm, 'Headers', descriptors[index])); + } +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + #[test] fn headers_prototype_methods_are_declared_operations() { let mut vm = new_storage_test_vm("https://headers-prototype-methods.test/"); @@ -7164,7 +7402,7 @@ fn headers_methods_reject_incompatible_receivers() { } #[test] -fn headers_declared_methods_preserve_descriptors_and_iterator_alias() { +fn response_headers_inherit_declared_methods_and_iterator_alias() { let mut vm = new_storage_test_vm("https://headers-declared-methods.test/"); let result = vm @@ -7172,6 +7410,9 @@ fn headers_declared_methods_preserve_descriptors_and_iterator_alias() { r#" (() => { const headers = new Response(null, { headers: [['X-A', '1']] }).headers; + const prototype = Object.getPrototypeOf(headers); + if (prototype !== Headers.prototype) throw new Error('Headers must use the intrinsic prototype'); + if (Reflect.ownKeys(headers).length !== 0) throw new Error('Headers methods must be inherited'); const descriptors = [ ['get', 1], ['has', 1], @@ -7184,7 +7425,7 @@ fn headers_declared_methods_preserve_descriptors_and_iterator_alias() { ['entries', 0], ['forEach', 1], ].map(([name, expectedLength]) => { - const descriptor = Object.getOwnPropertyDescriptor(headers, name); + const descriptor = Object.getOwnPropertyDescriptor(prototype, name); return [ name, typeof descriptor?.value, @@ -7196,7 +7437,7 @@ fn headers_declared_methods_preserve_descriptors_and_iterator_alias() { descriptor?.configurable, ].join(':'); }); - const iteratorDescriptor = Object.getOwnPropertyDescriptor(headers, Symbol.iterator); + const iteratorDescriptor = Object.getOwnPropertyDescriptor(prototype, Symbol.iterator); const iterated = Array.from(headers).map(([name, value]) => `${name}=${value}`).join(','); return JSON.stringify({ descriptors, @@ -7218,7 +7459,7 @@ fn headers_declared_methods_preserve_descriptors_and_iterator_alias() { assert_eq!( result, - r#"{"descriptors":["get:function:get:1:1:true:true:true","has:function:has:1:1:true:true:true","getSetCookie:function:getSetCookie:0:0:true:true:true","set:function:set:2:2:true:true:true","delete:function:delete:1:1:true:true:true","append:function:append:2:2:true:true:true","keys:function:keys:0:0:true:true:true","values:function:values:0:0:true:true:true","entries:function:entries:0:0:true:true:true","forEach:function:forEach:1:1:true:true:true"],"iterator":"function:true:entries:0:true:true:true:x-a=1"}"# + r#"{"descriptors":["get:function:get:1:1:true:true:true","has:function:has:1:1:true:true:true","getSetCookie:function:getSetCookie:0:0:true:true:true","set:function:set:2:2:true:true:true","delete:function:delete:1:1:true:true:true","append:function:append:2:2:true:true:true","keys:function:keys:0:0:true:true:true","values:function:values:0:0:true:true:true","entries:function:entries:0:0:true:true:true","forEach:function:forEach:1:1:true:true:true"],"iterator":"function:true:entries:0:false:true:true:x-a=1"}"# ); }