mirror of
https://github.com/lexmount/moli.git
synced 2026-09-23 00:01:25 +00:00
fix(fetch): preserve internal header lists across request copies
Keep normalized fields in their original order and combine them for public iteration and header consumers. Read inherited Request headers from private slots so mode changes preserve safe fields and do not invoke author getters. Preserve the lists through Headers mutation, Request/Response clone and Cache round trips, with shared Window/Worker regressions for duplicates, empty values, public projections, script overrides and body clones.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use super::headers::{build_headers_object, headers_entries, mark_headers_immutable};
|
||||
use super::headers::{build_headers_object, headers_entries, headers_list, mark_headers_immutable};
|
||||
use super::response::{ParsedResponseInit, install_response_body_methods, parse_response_init};
|
||||
use super::*;
|
||||
pub(in crate::network_host) use crate::util::constructor_prototype;
|
||||
@@ -181,7 +181,7 @@ pub(crate) fn request_headers_entries<'s>(
|
||||
request: v8::Local<'s, v8::Object>,
|
||||
) -> Vec<(String, String)> {
|
||||
request_slot_object(scope, request, REQUEST_HEADERS_SLOT)
|
||||
.map(|headers| headers_entries(scope, headers))
|
||||
.map(|headers| headers_list(scope, headers))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -828,7 +828,7 @@ fn request_clone_callback<'s>(
|
||||
}
|
||||
}
|
||||
let entries = request_slot_object(scope, this, REQUEST_HEADERS_SLOT)
|
||||
.map(|headers| headers_entries(scope, headers))
|
||||
.map(|headers| headers_list(scope, headers))
|
||||
.unwrap_or_default();
|
||||
let guard =
|
||||
if request_slot_string(scope, this, REQUEST_MODE_SLOT).as_deref() == Some("no-cors") {
|
||||
|
||||
@@ -9,7 +9,7 @@ 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, normalized_headers_entries};
|
||||
pub(super) use self::store::{
|
||||
build_headers_object, build_headers_object_with_state, headers_entries_from_init,
|
||||
build_headers_object, build_headers_object_with_state, headers_entries_from_init, headers_list,
|
||||
mark_headers_immutable,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ pub(super) fn clone_headers_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
original: v8::Local<'s, v8::Object>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let entries = headers_entries(scope, original);
|
||||
let entries = headers_list(scope, original);
|
||||
let guard = self::store::headers_guard(scope, original);
|
||||
let immutable = self::store::headers_are_immutable(scope, original);
|
||||
build_headers_object_with_state(scope, &entries, guard, immutable)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::super::store::{get_header_prop, headers_entries, normalized_header_name_or_throw};
|
||||
use super::super::store::{get_header_prop, headers_list, normalized_header_name_or_throw};
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(in crate::network_host::headers) fn headers_get_set_cookie_callback<'s>(
|
||||
let Some(this) = require_headers_receiver(scope, args.this()) else {
|
||||
return;
|
||||
};
|
||||
let values = headers_entries(scope, this)
|
||||
let values = headers_list(scope, this)
|
||||
.into_iter()
|
||||
.filter_map(|(name, value)| (name == "set-cookie").then_some(value))
|
||||
.filter_map(|value| v8_string(scope, &value).map(Into::into))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::super::store::{
|
||||
header_allowed_by_guard, header_append_allowed_by_guard, headers_are_immutable,
|
||||
headers_entries, headers_guard, normalized_header_name_or_throw,
|
||||
normalized_header_value_or_throw, set_headers_entries,
|
||||
header_allowed_by_guard, header_append_allowed_by_guard, headers_are_immutable, headers_guard,
|
||||
headers_list, normalized_header_name_or_throw, normalized_header_value_or_throw,
|
||||
set_headers_entries,
|
||||
};
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
@@ -59,7 +59,7 @@ pub(in crate::network_host::headers) fn headers_set_callback<'s>(
|
||||
if !header_allowed_by_guard(guard, &lower, &value) {
|
||||
return;
|
||||
}
|
||||
let mut entries = headers_entries(scope, this);
|
||||
let mut entries = headers_list(scope, this);
|
||||
let insert_at = entries
|
||||
.iter()
|
||||
.position(|(entry_name, _)| *entry_name == lower)
|
||||
@@ -87,7 +87,7 @@ pub(in crate::network_host::headers) fn headers_delete_callback<'s>(
|
||||
let Some(lower) = normalized_header_name_or_throw(scope, &name) else {
|
||||
return;
|
||||
};
|
||||
let mut entries = headers_entries(scope, this);
|
||||
let mut entries = headers_list(scope, this);
|
||||
entries.retain(|(entry_name, _)| *entry_name != lower);
|
||||
set_headers_entries(scope, this, &entries);
|
||||
}
|
||||
@@ -115,7 +115,7 @@ pub(in crate::network_host::headers) fn headers_append_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let guard = headers_guard(scope, this);
|
||||
let mut entries = headers_entries(scope, this);
|
||||
let mut entries = headers_list(scope, this);
|
||||
if !header_append_allowed_by_guard(guard, &name, &value, &entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ pub(crate) use self::entries::{
|
||||
pub(in crate::network_host::headers) use self::entries::{
|
||||
header_allowed_by_guard, header_append_allowed_by_guard, headers_are_immutable, headers_guard,
|
||||
};
|
||||
pub(in crate::network_host) use self::entries::{mark_headers_immutable, set_headers_entries};
|
||||
pub(in crate::network_host) use self::entries::{
|
||||
headers_list, mark_headers_immutable, set_headers_entries,
|
||||
};
|
||||
pub(in crate::network_host::headers) use self::entries::{
|
||||
normalized_header_name_or_throw, normalized_header_value_or_throw,
|
||||
};
|
||||
|
||||
@@ -144,20 +144,26 @@ pub(in crate::network_host) fn set_headers_entries(
|
||||
}
|
||||
|
||||
pub(in crate::network_host) fn headers_entries_json(entries: &[(String, String)]) -> String {
|
||||
let entries = normalized_headers_entries(entries);
|
||||
// Keep each field in the header list. Refilling a Request after changing
|
||||
// its guard must append these fields individually, in their original order.
|
||||
let entries = normalized_header_list(entries);
|
||||
serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_owned())
|
||||
}
|
||||
|
||||
fn normalized_header_list(entries: &[(String, String)]) -> Vec<(String, String)> {
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|(name, value)| {
|
||||
let name = normalized_header_name(name)?;
|
||||
let value = normalize_header_value(value);
|
||||
is_valid_header_value(&value).then_some((name, value))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_headers_entries(entries: &[(String, String)]) -> Vec<(String, String)> {
|
||||
let mut normalized = Vec::<(String, String)>::new();
|
||||
for (name, value) in entries {
|
||||
let Some(lower) = normalized_header_name(name) else {
|
||||
continue;
|
||||
};
|
||||
let value = normalize_header_value(value);
|
||||
if !is_valid_header_value(&value) {
|
||||
continue;
|
||||
}
|
||||
for (lower, value) in normalized_header_list(entries) {
|
||||
if lower == "set-cookie" {
|
||||
normalized.push((lower, value));
|
||||
continue;
|
||||
@@ -232,19 +238,22 @@ fn is_http_whitespace(ch: char) -> bool {
|
||||
matches!(ch, '\t' | '\n' | '\r' | ' ')
|
||||
}
|
||||
|
||||
/// The sorted, combined view used by public iteration and header consumers.
|
||||
pub(crate) fn headers_entries<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
obj: v8::Local<'s, v8::Object>,
|
||||
) -> Vec<(String, String)> {
|
||||
headers_entries_if_present(scope, obj).unwrap_or_default()
|
||||
normalized_headers_entries(&headers_list(scope, obj))
|
||||
}
|
||||
|
||||
pub(in crate::network_host) fn headers_entries_if_present<'s>(
|
||||
/// The internal header list, before the public sort-and-combine projection.
|
||||
pub(in crate::network_host) fn headers_list<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
obj: v8::Local<'s, v8::Object>,
|
||||
) -> Option<Vec<(String, String)>> {
|
||||
) -> Vec<(String, String)> {
|
||||
private_string_value(scope, obj, HEADERS_ENTRIES_SLOT)
|
||||
.and_then(|json| serde_json::from_str::<Vec<(String, String)>>(&json).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn private_string_value<'s>(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::super::*;
|
||||
use super::entries::{
|
||||
HEADERS_ENTRIES_SLOT, HEADERS_GUARD_SLOT, HEADERS_IMMUTABLE_SLOT, HeadersGuard,
|
||||
headers_entries, headers_entries_json, normalized_header_name_or_throw,
|
||||
headers_entries_json, headers_list, normalized_header_name_or_throw,
|
||||
};
|
||||
use crate::web_api_interfaces;
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
@@ -23,7 +23,7 @@ pub(in crate::network_host) fn get_header_prop<'s>(
|
||||
name: &str,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
let lower = normalized_header_name_or_throw(scope, name)?;
|
||||
let values = headers_entries(scope, obj)
|
||||
let values = headers_list(scope, obj)
|
||||
.into_iter()
|
||||
.filter_map(|(entry_name, value)| (entry_name == lower).then_some(value))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::super::headers::HeadersGuard;
|
||||
use super::super::headers::{HeadersGuard, headers_list};
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
|
||||
@@ -244,15 +244,11 @@ fn request_input_snapshot_from_private_slots<'s>(
|
||||
webidl::WebIdlError::custom_message("Failed to materialize request body")
|
||||
})?
|
||||
};
|
||||
let headers = webidl::property_result(
|
||||
scope,
|
||||
object,
|
||||
"headers",
|
||||
webidl::Context::member("Request", "headers"),
|
||||
)?
|
||||
.map(|value| headers_entries_from_init(scope, value))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
// A Request copies its internal list, without consulting a script-defined
|
||||
// headers getter or the Headers object's public iterator.
|
||||
let headers = request_slot_object(scope, object, REQUEST_HEADERS_SLOT)
|
||||
.map(|headers| headers_list(scope, headers))
|
||||
.unwrap_or_default();
|
||||
let signal = request_slot_value(scope, object, REQUEST_SIGNAL_SLOT)
|
||||
.map(|value| request_signal_snapshot_from_value(scope, value))
|
||||
.transpose()?
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::super::fetch_surface::{
|
||||
response_slot_number,
|
||||
};
|
||||
use super::*;
|
||||
use crate::network_host::headers::headers_list;
|
||||
use crate::types::NetworkBodySourceId;
|
||||
use moli_fetch::{RequestMode, RequestRedirectMode};
|
||||
use moli_url::WebOrigin;
|
||||
@@ -698,7 +699,7 @@ fn materialize_response_head_for_purpose<'s>(
|
||||
let status_text =
|
||||
response_slot_string(scope, response, RESPONSE_STATUS_TEXT_SLOT).unwrap_or_default();
|
||||
let headers = response_slot_object(scope, response, RESPONSE_HEADERS_SLOT)
|
||||
.map(|headers| headers_entries(scope, headers))
|
||||
.map(|headers| headers_list(scope, headers))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Cache admission uses the public response before restoring its internal
|
||||
@@ -779,7 +780,7 @@ fn restore_response_internal_head<'s>(
|
||||
response_slot_string(scope, response, RESPONSE_INTERNAL_STATUS_TEXT_SLOT)
|
||||
.unwrap_or_default();
|
||||
head.headers = response_slot_object(scope, response, RESPONSE_INTERNAL_HEADERS_SLOT)
|
||||
.map(|headers| headers_entries(scope, headers))
|
||||
.map(|headers| headers_list(scope, headers))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn headers_preserve_internal_lists_in_window_and_worker() {
|
||||
let fixture = include_str!("../../../tests/fixtures/headers-list.js");
|
||||
for worker in [false, true] {
|
||||
let loader = static_http_loader([]);
|
||||
let mut vm =
|
||||
new_page_task_executor_test_vm_with_loader("https://headers-list.test/", &loader);
|
||||
vm.eval("globalThis.headerListResult = null;").unwrap();
|
||||
let script = if worker {
|
||||
let source = format!(
|
||||
"{fixture}\nheadersListProbe('https://headers-list.test', false, true).then(postMessage, error => postMessage({{error: String(error.stack || error)}}));"
|
||||
);
|
||||
format!(
|
||||
r#"
|
||||
const workerUrl = URL.createObjectURL(new Blob([{}], {{type: 'text/javascript'}}));
|
||||
const worker = new Worker(workerUrl);
|
||||
const finish = value => {{
|
||||
headerListResult = value;
|
||||
worker.terminate();
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
}};
|
||||
worker.onmessage = event => finish(event.data);
|
||||
worker.onerror = event => {{ finish({{error: event.message}}); event.preventDefault(); }};
|
||||
"#,
|
||||
serde_json::to_string(&source).unwrap()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{fixture}\nheadersListProbe('https://headers-list.test', false, true).then(value => {{ headerListResult = value; }}, error => {{ headerListResult = {{error: String(error.stack || error)}}; }});"
|
||||
)
|
||||
};
|
||||
vm.eval(&script).unwrap();
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(headerListResult !== null)",
|
||||
"true",
|
||||
"Headers list checks should finish",
|
||||
)
|
||||
.await;
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(&vm.eval("JSON.stringify(headerListResult)").unwrap()).unwrap();
|
||||
let checks = result["checks"]
|
||||
.as_array()
|
||||
.unwrap_or_else(|| panic!("worker={worker}: {result}"));
|
||||
let failures: Vec<_> = checks
|
||||
.iter()
|
||||
.filter(|check| check["pass"] != true)
|
||||
.collect();
|
||||
assert_eq!(result["state"], "pass", "worker={worker}: {failures:?}");
|
||||
assert_eq!(checks.len(), 187, "worker={worker}");
|
||||
}
|
||||
}
|
||||
@@ -15827,6 +15827,7 @@ mod dom_xhr;
|
||||
mod fetch_integrity;
|
||||
mod fetch_referrer;
|
||||
mod fetch_request_guard;
|
||||
mod headers_list;
|
||||
mod http_fixture;
|
||||
mod indexed_db;
|
||||
mod inspector_unwrap;
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
async function headersListProbe(base, transport = true, cacheStorage = transport) {
|
||||
const checks = [];
|
||||
const check = (label, actual, wanted) => {
|
||||
if (typeof actual === 'function') {
|
||||
try { actual = actual(); } catch (error) { actual = 'throw:' + error.message; }
|
||||
}
|
||||
checks.push({label, actual, wanted, pass: JSON.stringify(actual) === JSON.stringify(wanted)});
|
||||
};
|
||||
const cases = [];
|
||||
for (const name of ['Accept', 'Accept-Language', 'Content-Language']) {
|
||||
for (const [label, values, wanted, mergedAllowed] of [
|
||||
['overflow', ['a'.repeat(126), 'b'], 'a'.repeat(126), false],
|
||||
['at-limit', ['a'.repeat(125), 'b'], 'a'.repeat(125) + ', b', true],
|
||||
['after-limit', ['a'.repeat(125), 'b', 'c'], 'a'.repeat(125) + ', b', false],
|
||||
['empty-first', ['', 'b'.repeat(127)], '', false],
|
||||
]) cases.push([name + '/' + label, name, values, wanted, mergedAllowed]);
|
||||
}
|
||||
const longType = 'text/plain;x=' + 'a'.repeat(114);
|
||||
cases.push(
|
||||
['type/duplicate', 'Content-Type', ['text/plain', 'text/plain'], 'text/plain', false],
|
||||
['type/parameter', 'Content-Type', ['text/plain;charset=utf8', 'extra'], 'text/plain;charset=utf8, extra', true],
|
||||
['type/parameter-second-type', 'Content-Type', ['text/plain;charset=utf8', 'application/json'], 'text/plain;charset=utf8, application/json', true],
|
||||
['type/unsafe', 'Content-Type', ['text/plain;charset=utf8', '"'], 'text/plain;charset=utf8', false],
|
||||
['type/invalid-first', 'Content-Type', ['application/json', 'text/plain'], 'text/plain', false],
|
||||
['type/overflow', 'Content-Type', [longType, 'ok'], longType, false],
|
||||
);
|
||||
for (const [label, name, values, wanted, mergedAllowed] of cases) {
|
||||
const names = [name, name.toLowerCase(), name.toUpperCase()];
|
||||
const pairs = values.map((value, index) => [names[index], value]);
|
||||
const url = base + '/echo?case=' + encodeURIComponent(label);
|
||||
const source = new Request(url, {headers: pairs});
|
||||
const joined = values.join(', ');
|
||||
const refill = request => new Request(request, {mode: 'no-cors'});
|
||||
check(label + '/mode-change', refill(source).headers.get(name), wanted);
|
||||
check(label + '/clone-refill', refill(source.clone()).headers.get(name), wanted);
|
||||
check(label + '/construct-copy', refill(new Request(source)).headers.get(name), wanted);
|
||||
check(label + '/refill-cors', refill(new Request(source, {cache: 'no-store'})).headers.get(name), wanted);
|
||||
check(label + '/input-unmodified', source.headers.get(name), joined);
|
||||
check(label + '/guarded-clone', refill(source).clone().headers.get(name), wanted);
|
||||
check(label + '/public-headers-init', new Request(url, {mode: 'no-cors', headers: source.headers}).headers.get(name), mergedAllowed ? joined : null);
|
||||
source.headers.append('X-Unrelated', 'value');
|
||||
check(label + '/after-unrelated-mutation', refill(source).headers.get(name), wanted);
|
||||
if (transport) {
|
||||
for (const [kind, input, init, expected] of [
|
||||
['mode', source, {mode: 'no-cors'}, wanted],
|
||||
['clone', source.clone(), {mode: 'no-cors'}, wanted],
|
||||
['refilled', refill(source), undefined, wanted],
|
||||
['cors', source, undefined, joined],
|
||||
]) {
|
||||
let actual;
|
||||
try { actual = new Headers((await (await fetch(input, init)).json()).headers).get(name); }
|
||||
catch (error) { actual = 'throw:' + error.message; }
|
||||
check(label + '/fetch-' + kind, actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mutating another name must not trim separators generated by empty fields.
|
||||
const pairs = [['X-B', 'b'], ['X-A', ' a '], ['Set-Cookie', 'a=1'], ['x-a', ''], ['X-A', ''], ['set-cookie', 'b=2']];
|
||||
const headers = new Headers(pairs);
|
||||
headers.append('X-C', 'c');
|
||||
headers.set('X-B', 'new');
|
||||
headers.delete('X-C');
|
||||
const expectedEntries = [['set-cookie', 'a=1'], ['set-cookie', 'b=2'], ['x-a', 'a, , '], ['x-b', 'new']];
|
||||
check('projection/get', headers.get('x-a'), 'a, , ');
|
||||
check('projection/entries', [...headers], expectedEntries);
|
||||
check('projection/keys', [...headers.keys()], expectedEntries.map(entry => entry[0]));
|
||||
check('projection/values', [...headers.values()], expectedEntries.map(entry => entry[1]));
|
||||
check('projection/cookies', headers.getSetCookie(), ['a=1', 'b=2']);
|
||||
const visited = [];
|
||||
headers.forEach((value, key, owner) => visited.push([key, value, owner === headers]));
|
||||
check('projection/forEach', visited, expectedEntries.map(entry => [...entry, true]));
|
||||
check('projection/public-copy-normalizes', new Headers(headers).get('x-a'), 'a, ,');
|
||||
const iterator = headers.entries();
|
||||
check('projection/live-first', iterator.next().value, expectedEntries[0]);
|
||||
headers.append('X-B', 'tail');
|
||||
check('projection/live-rest', [...iterator], [expectedEntries[1], expectedEntries[2], ['x-b', 'new, tail']]);
|
||||
headers.set('x-a', 'reset');
|
||||
headers.append('X-A', 'end');
|
||||
check('projection/set-replaces-all', headers.get('x-a'), 'reset, end');
|
||||
headers.delete('x-a');
|
||||
check('projection/delete-all', headers.has('x-a'), false);
|
||||
const response = new Response(null, {headers: pairs});
|
||||
response.headers.append('x-other', 'value');
|
||||
const responseCopy = response.clone();
|
||||
responseCopy.headers.set('x-b', 'changed');
|
||||
check('response/original-empty-values', response.headers.get('x-a'), 'a, , ');
|
||||
check('response/clone-empty-values', responseCopy.headers.get('x-a'), 'a, , ');
|
||||
check('response/clone-isolated', response.headers.get('x-b'), 'b');
|
||||
|
||||
const long = 'a'.repeat(126);
|
||||
for (const poisoned of ['request-getter', 'headers-iterator', 'both']) {
|
||||
const request = new Request(base + '/echo?case=' + poisoned, {headers: [['Accept', long], ['accept', 'b']]});
|
||||
const associated = request.headers;
|
||||
let getterCalls = 0;
|
||||
let iteratorCalls = 0;
|
||||
if (poisoned !== 'headers-iterator') {
|
||||
Object.defineProperty(request, 'headers', {get() { getterCalls++; throw new Error('headers getter'); }});
|
||||
}
|
||||
if (poisoned !== 'request-getter') {
|
||||
Object.defineProperty(associated, Symbol.iterator, {get() { iteratorCalls++; throw new Error('headers iterator'); }});
|
||||
}
|
||||
check(poisoned + '/inherit', () => new Request(request).headers.get('Accept'), long + ', b');
|
||||
check(poisoned + '/refill', () => new Request(request, {mode: 'no-cors'}).headers.get('Accept'), long);
|
||||
check(poisoned + '/clone', () => request.clone().headers.get('Accept'), long + ', b');
|
||||
if (transport) {
|
||||
let actual;
|
||||
try { actual = new Headers((await (await fetch(request, {mode: 'no-cors'})).json()).headers).get('Accept'); }
|
||||
catch (error) { actual = 'throw:' + error.message; }
|
||||
check(poisoned + '/fetch', actual, long);
|
||||
}
|
||||
check(poisoned + '/getter-not-read', getterCalls, 0);
|
||||
check(poisoned + '/iterator-not-read', iteratorCalls, 0);
|
||||
if (poisoned !== 'request-getter') {
|
||||
check(poisoned + '/explicit-init-observes-iterator', () => new Request(base, {headers: associated}), 'throw:headers iterator');
|
||||
check(poisoned + '/explicit-init-reads-once', iteratorCalls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const stream of [false, true]) {
|
||||
const body = stream ? new ReadableStream({start(controller) { controller.enqueue(new Uint8Array([65])); controller.close(); }}) : 'A';
|
||||
const source = new Request(base, {method: 'POST', body, duplex: 'half', headers: [['Accept', long], ['accept', 'b']]});
|
||||
const copy = source.clone();
|
||||
const refilled = new Request(copy, {mode: 'no-cors', body: 'replacement'});
|
||||
check('body/' + stream + '/clone-refill', refilled.headers.get('accept'), long);
|
||||
check('body/' + stream + '/replacement', await refilled.text(), 'replacement');
|
||||
check('body/' + stream + '/original', await source.text(), 'A');
|
||||
}
|
||||
|
||||
// Cache stores copies of Requests and Responses, including their header lists.
|
||||
if (cacheStorage) {
|
||||
const cacheName = 'headers-list-' + Math.random().toString(36);
|
||||
try {
|
||||
const cache = await caches.open(cacheName);
|
||||
const url = base + '/cached';
|
||||
const request = new Request(url, {headers: [['Accept', long], ['accept', 'b']]});
|
||||
await cache.put(request, new Response('stored', {headers: [['Vary', 'Accept'], ['X-List', 'first'], ['x-list', '']]}));
|
||||
const keys = await cache.keys();
|
||||
check('cache/key-refill', new Request(keys[0], {mode: 'no-cors'}).headers.get('Accept'), long);
|
||||
const match = await cache.match(new Request(url, {headers: {Accept: long + ', b'}}));
|
||||
check('cache/vary-combined-match', await match.text(), 'stored');
|
||||
check('cache/response-fields', match.headers.get('x-list'), 'first, ');
|
||||
check('cache/vary-mismatch', await cache.match(new Request(url, {headers: {Accept: long}})), undefined);
|
||||
} finally { await caches.delete(cacheName); }
|
||||
}
|
||||
return {state: checks.every(check => check.pass) ? 'pass' : 'fail', checks};
|
||||
}
|
||||
@@ -150,7 +150,7 @@ wait_until = "load"
|
||||
timeout_ms = 5000
|
||||
suite = "smoke"
|
||||
tags = ["fetch", "headers", "request", "response", "exceptions"]
|
||||
notes = "Manual regression port for pending V8 exception propagation through Headers iterable initialization, Request(input) headers snapshotting, and Response.json(init) headers processing."
|
||||
notes = "Manual regression port for pending V8 exception propagation through Headers initialization and explicit Request/Response init headers, plus Request copies that ignore an overridden public headers getter."
|
||||
|
||||
[[test]]
|
||||
id = "response-basic"
|
||||
|
||||
@@ -118,15 +118,31 @@ test(function () {
|
||||
const request = new Request("https://example.test/headers", {
|
||||
headers: [["x-test", "one"]],
|
||||
});
|
||||
let getterCalls = 0;
|
||||
Object.defineProperty(request, "headers", {
|
||||
get() {
|
||||
getterCalls += 1;
|
||||
throw new RangeError("request headers getter failed");
|
||||
},
|
||||
});
|
||||
assert_equals(new Request(request).headers.get("x-test"), "one",
|
||||
"Request(input) should copy the internal header list");
|
||||
assert_equals(request.clone().headers.get("x-test"), "one",
|
||||
"Request.clone() should copy the internal header list");
|
||||
assert_equals(getterCalls, 0, "internal copies should not read the public headers getter");
|
||||
}, "Request copies ignore an overridden public headers getter");
|
||||
|
||||
test(function () {
|
||||
const init = {};
|
||||
Object.defineProperty(init, "headers", {
|
||||
get() {
|
||||
throw new RangeError("request init headers getter failed");
|
||||
},
|
||||
});
|
||||
assert_throws_name("RangeError", function () {
|
||||
new Request(request);
|
||||
}, "Request(input) should propagate a throwing input headers getter");
|
||||
}, "Request constructor propagates throwing input headers getter");
|
||||
new Request("https://example.test/headers", init);
|
||||
}, "Request init should propagate a throwing headers getter");
|
||||
}, "Request constructor propagates throwing init headers getter");
|
||||
|
||||
test(function () {
|
||||
const init = {};
|
||||
|
||||
Reference in New Issue
Block a user