mirror of
https://github.com/lexmount/moli.git
synced 2026-09-30 00:01:36 +00:00
fix(fetch): preserve RequestInit conversion exceptions
Read headers and signal through exception-preserving helpers. Capture conversion exceptions around Window and Worker fetch argument parsing so rejected promises retain the original thrown value, including primitives, instead of replacing it with a TypeError or starting a request. Cover constructor and fetch exception identity, unused input bodies, no network dispatch, and cross-realm Promise ownership. Validation: cargo fmt, workspace Clippy with warnings denied, and nextest (17987 passed, 13 skipped). 68 upstream WPT cases show no regressions. All 832 additional checks pass, including the eight pre-existing getter exception failures identified during the preceding blob URL fix.
This commit is contained in:
@@ -131,15 +131,15 @@ pub(in crate::network_host) use self::preflight_events::CorsPreflightNetworkObse
|
||||
pub(in crate::network_host) use self::request::normalize_request_method;
|
||||
pub(crate) use self::request::request_constructor_callback;
|
||||
pub(crate) use self::request::{FetchArgumentError, RequestUrlError, convert_fetch_arguments};
|
||||
pub(crate) use self::request::{
|
||||
convert_fetch_arguments, parse_fetch_init, parse_request_redirect_mode_label,
|
||||
request_object_credentials_mode, validate_fetch_body,
|
||||
};
|
||||
pub(crate) use self::request::{
|
||||
mark_request_input_body_used_for_fetch, request_input_snapshot,
|
||||
try_resolve_request_constructor_url, try_resolve_request_constructor_url_for_base,
|
||||
try_resolve_request_constructor_url_for_child, validate_request_url_credentials,
|
||||
};
|
||||
pub(crate) use self::request::{
|
||||
parse_fetch_init, parse_request_redirect_mode_label, request_object_credentials_mode,
|
||||
validate_fetch_body,
|
||||
};
|
||||
pub(crate) use self::request_scope::effective_subresource_policy_context;
|
||||
pub(in crate::network_host) use self::request_scope::{
|
||||
XHR_CHILD_CONTEXT_HANDLE_SLOT, active_subresource_network_partition_key,
|
||||
|
||||
@@ -13,7 +13,8 @@ pub(crate) use self::error::{FetchArgumentError, RequestUrlError};
|
||||
pub(crate) use self::init::convert_fetch_arguments;
|
||||
pub(in crate::network_host) use self::init::request_credentials_mode_label;
|
||||
pub(crate) use self::init::{
|
||||
RequestInitValidation, parse_fetch_init, request_object_credentials_mode, validate_fetch_body,
|
||||
RequestInitValidation, convert_fetch_arguments, parse_fetch_init,
|
||||
request_object_credentials_mode, validate_fetch_body,
|
||||
};
|
||||
pub(crate) use self::init::{parse_request_redirect_mode_label, request_redirect_mode_label};
|
||||
pub(in crate::network_host) use self::input::normalize_request_method;
|
||||
|
||||
@@ -4478,6 +4478,73 @@ fn check_blob_fetch_and_xhr_methods_in_window_and_worker(
|
||||
}))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming_input() {
|
||||
run_page_vm_async_test(async move {
|
||||
for worker in [false, true] {
|
||||
let mut page_vm = test_page_vm();
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
let probe = r#"(async () => {
|
||||
const check = (value, message) => { if (!value) throw new Error(message); };
|
||||
const url = 'data:text/plain,must-not-fetch';
|
||||
for (const api of ['Request', 'fetch']) {
|
||||
for (const member of ['method', 'headers', 'signal']) {
|
||||
for (const sentinel of [undefined, null, false, 0, 'sentinel', {}, new Error('sentinel')]) {
|
||||
const input = new Request(url, {method:'POST', body:'kept'});
|
||||
const init = {};
|
||||
Object.defineProperty(init, member, {get() { throw sentinel; }});
|
||||
let result, caught, threw = false;
|
||||
try { result = api === 'Request' ? new Request(input, init) : fetch(input, init); }
|
||||
catch (error) { threw = true; caught = error; }
|
||||
if (api === 'Request') {
|
||||
check(threw && Object.is(caught, sentinel), 'Request must rethrow ' + member);
|
||||
} else {
|
||||
check(!threw && result instanceof Promise, 'fetch conversion must return a Promise');
|
||||
let rejected = false;
|
||||
await result.then(() => {}, error => { rejected = true; caught = error; });
|
||||
check(rejected && Object.is(caught, sentinel), 'fetch must reject with original ' + member);
|
||||
}
|
||||
check(!input.bodyUsed, 'failed ' + member + ' conversion consumed input body');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof document !== 'undefined') {
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
const other = frame.contentWindow;
|
||||
const sentinel = new other.Error('cross-realm');
|
||||
const promise = other.fetch.call(window, url, {get headers() { throw sentinel; }});
|
||||
check(promise instanceof other.Promise, 'conversion rejection must belong to function realm');
|
||||
let caught;
|
||||
await promise.catch(error => { caught = error; });
|
||||
check(caught === sentinel, 'cross-realm exception identity');
|
||||
frame.remove();
|
||||
}
|
||||
return 'ok';
|
||||
})()"#;
|
||||
let script = if worker {
|
||||
let source = serde_json::to_string(&format!("{probe}.then(postMessage, error => postMessage(String(error)))")).unwrap();
|
||||
format!(r#"globalThis.__initExceptionResult = 'pending';
|
||||
const source = URL.createObjectURL(new Blob([{source}]));
|
||||
const worker = new Worker(source);
|
||||
worker.onmessage = e => {{ globalThis.__initExceptionResult = e.data; worker.terminate(); URL.revokeObjectURL(source); }};
|
||||
worker.onerror = e => {{ globalThis.__initExceptionResult = e.message; }};"#)
|
||||
} else {
|
||||
format!("globalThis.__initExceptionResult = 'pending'; {probe}.then(value => {{ globalThis.__initExceptionResult = value; }}, error => {{ globalThis.__initExceptionResult = String(error); }})")
|
||||
};
|
||||
let (result, network_output) = local_executor.run(async move {
|
||||
page_vm.vm_mut().eval(&script)?;
|
||||
drive_websocket_until_done(&mut page_vm, "String(globalThis.__initExceptionResult !== 'pending')", "RequestInit exception checks should finish").await?;
|
||||
let result = page_vm.vm_mut().eval("globalThis.__initExceptionResult")?;
|
||||
Ok::<_, anyhow::Error>((result, page_vm.vm_mut().take_network_output()))
|
||||
}).await.expect("RequestInit exceptions should run on owner lane");
|
||||
assert_eq!(result, "ok", "worker={worker}");
|
||||
let (records, _, _) = split_network_output_items(network_output);
|
||||
assert!(records.iter().all(|record| record.resource_type() != SubresourceResourceType::Fetch), "failed argument conversion dispatched a fetch; worker={worker}");
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blob_url_entries_survive_request_cloning_and_xhr_open_in_window_and_worker() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
Reference in New Issue
Block a user