From eb3e9b772b3efb8be00fdfb70eb95e6a5f84a2e1 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 23 Sep 2026 17:31:25 +0800 Subject: [PATCH] refactor(fetch): preserve typed argument conversion errors Carry WebIdlError and structured fetch validation errors through Window and Worker argument conversion, formatting them only at the rejection boundary. Propagate Proxy.has exceptions before further conversion or body consumption, and cover exception identity, validation messages, realms, and side effects. Fix existing XHR response-document header type mismatches in the parser and its regression fixture so the workspace builds with byte-valued headers. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast (18490 passed, 14 skipped) --- moli-renderer-v8/src/network_host.rs | 2 +- .../src/network_host/fetch/bindings.rs | 31 +++++----- .../network_host/fetch/bindings/request.rs | 3 +- .../src/network_host/fetch/input.rs | 19 +++--- moli-renderer-v8/src/network_host/request.rs | 2 + .../src/network_host/request/error.rs | 62 +++++++++++++++++++ .../src/network_host/request/init.rs | 50 +++++++++------ .../src/network_host/request/input.rs | 12 +++- .../src/network_host/url_helpers.rs | 47 +++++++++++++- .../src/network_host/xhr/send/request.rs | 4 +- .../src/runtime/page_vm/tests/fetch_xhr.rs | 54 +++++++++++++++- .../src/worker/global_scope/fetch.rs | 54 ++++++++-------- .../src/worker/global_scope/xhr.rs | 2 +- 13 files changed, 256 insertions(+), 86 deletions(-) create mode 100644 moli-renderer-v8/src/network_host/request/error.rs diff --git a/moli-renderer-v8/src/network_host.rs b/moli-renderer-v8/src/network_host.rs index b0722c4620..c2d9c58be9 100644 --- a/moli-renderer-v8/src/network_host.rs +++ b/moli-renderer-v8/src/network_host.rs @@ -123,9 +123,9 @@ pub(crate) use self::media::{ start_media_element_resource_fetch, }; pub(in crate::network_host) use self::preflight_events::CorsPreflightNetworkObserver; -pub(crate) use self::request::convert_fetch_arguments; 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, convert_fetch_arguments}; 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, diff --git a/moli-renderer-v8/src/network_host/fetch/bindings.rs b/moli-renderer-v8/src/network_host/fetch/bindings.rs index fd9460031f..ab9ccdab82 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings.rs @@ -353,23 +353,25 @@ fn window_fetch_callback_in_relevant_realm<'s>( fn window_fetch_signal_value<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, -) -> Result>, String> { +) -> Result>, crate::webidl::WebIdlError> { let signal_key = v8str(scope, "signal"); if args.length() > 1 { let init_arg = args.get(1); if !init_arg.is_null_or_undefined() && let Ok(init) = v8::Local::::try_from(init_arg) - && init - .has(scope, signal_key.into()) - .ok_or("Failed to read RequestInit.signal")? + && init.has(scope, signal_key.into()).ok_or_else(|| { + crate::webidl::WebIdlError::pending_exception(crate::webidl::Context::member( + "RequestInit", + "signal", + )) + })? { return crate::webidl::property_result( scope, init, "signal", crate::webidl::Context::member("RequestInit", "signal"), - ) - .map_err(|error| error.to_string()); + ); } } @@ -383,8 +385,7 @@ fn window_fetch_signal_value<'s>( request_like, "signal", crate::webidl::Context::member("Request", "signal"), - ) - .map_err(|error| error.to_string()); + ); } Ok(None) @@ -394,19 +395,19 @@ fn validate_window_fetch_signal<'s>( scope: &mut v8::PinScope<'s, '_>, host: &mut JsContextHost, value: v8::Local<'s, v8::Value>, -) -> Result>, String> { +) -> Result>, crate::webidl::WebIdlError> { if value.is_null_or_undefined() { return Ok(None); } let Ok(signal) = v8::Local::::try_from(value) else { - return Err( - "Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.".to_owned(), - ); + return Err(crate::webidl::WebIdlError::custom_message( + "Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.", + )); }; if !host.is_abort_signal(scope, signal) { - return Err( - "Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.".to_owned(), - ); + return Err(crate::webidl::WebIdlError::custom_message( + "Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.", + )); } Ok(Some(signal)) } diff --git a/moli-renderer-v8/src/network_host/fetch/bindings/request.rs b/moli-renderer-v8/src/network_host/fetch/bindings/request.rs index f7df440a69..20e0c22207 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/request.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/request.rs @@ -74,7 +74,8 @@ pub(super) fn prepare_window_fetch_request<'s>( let cors_preflight_request_headers = request_headers.clone(); let request_headers = merge_byte_string_request_headers(host.extra_http_headers(), &request_headers); - let resolved_url = resolve_context_url(&base_url, &parsed.url, None)?; + let resolved_url = + resolve_context_url(&base_url, &parsed.url, None).map_err(|error| error.to_string())?; Ok(PreparedWindowFetchRequest { frame_id, diff --git a/moli-renderer-v8/src/network_host/fetch/input.rs b/moli-renderer-v8/src/network_host/fetch/input.rs index f5cdb04f2d..3137d83738 100644 --- a/moli-renderer-v8/src/network_host/fetch/input.rs +++ b/moli-renderer-v8/src/network_host/fetch/input.rs @@ -22,15 +22,14 @@ pub(super) struct ParsedWindowFetchInput { pub(super) fn parse_window_fetch_input<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, -) -> Result { +) -> Result { if args.length() < 1 { return Err( - webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1)) - .to_string(), + webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1)).into(), ); } let arg0 = args.get(0); - let inherited = request_input_snapshot(scope, arg0).map_err(|error| error.to_string())?; + let inherited = request_input_snapshot(scope, arg0)?; if let Some(inherited) = inherited { let req_obj = v8::Local::::try_from(arg0).expect("request-like object"); let url = inherited.url.clone(); @@ -130,13 +129,14 @@ pub(super) fn parse_window_fetch_input<'s>( fn validate_no_cors_method( request_mode: moli_fetch::RequestMode, method: &str, -) -> Result<(), String> { +) -> Result<(), FetchArgumentError> { if request_mode == moli_fetch::RequestMode::NoCors && !moli_fetch::is_cors_safelisted_method(method) { - return Err(format!( - "Failed to execute 'fetch': method `{method}` is unsupported in no-cors mode." - )); + return Err(FetchArgumentError::UnsupportedNoCorsMethod { + method: method.to_owned(), + interface: None, + }); } Ok(()) } @@ -144,8 +144,7 @@ fn validate_no_cors_method( fn fetch_request_info_url<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, -) -> Result { +) -> Result { webidl::convert::(scope, value, webidl::Context::argument("fetch", 1)) .map(Into::into) - .map_err(|error| error.to_string()) } diff --git a/moli-renderer-v8/src/network_host/request.rs b/moli-renderer-v8/src/network_host/request.rs index 645e2f00e6..097159846e 100644 --- a/moli-renderer-v8/src/network_host/request.rs +++ b/moli-renderer-v8/src/network_host/request.rs @@ -1,4 +1,5 @@ mod bindings; +mod error; mod init; mod input; @@ -8,6 +9,7 @@ use super::headers::{ use super::*; pub(crate) use self::bindings::request_constructor_callback; +pub(crate) use self::error::FetchArgumentError; 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::{parse_fetch_init, request_object_credentials_mode}; diff --git a/moli-renderer-v8/src/network_host/request/error.rs b/moli-renderer-v8/src/network_host/request/error.rs new file mode 100644 index 0000000000..e337deadb5 --- /dev/null +++ b/moli-renderer-v8/src/network_host/request/error.rs @@ -0,0 +1,62 @@ +use crate::network_host::url_helpers::ResolveContextUrlError; +use crate::webidl; +use std::fmt; + +/// Argument conversion failures remain structured until the fetch binding +/// turns them into a JavaScript rejection. +#[derive(Debug)] +pub(crate) enum FetchArgumentError { + WebIdl(webidl::WebIdlError), + UnsupportedNoCorsMethod { + method: String, + interface: Option<&'static str>, + }, + Url(ResolveContextUrlError), +} + +impl FetchArgumentError { + pub(super) fn throw(&self, scope: &mut v8::PinScope<'_, '_>) { + match self { + Self::WebIdl(error) => webidl::throw_error(scope, error), + _ => webidl::throw_type_error(scope, &self.to_string()), + } + } +} + +impl From for FetchArgumentError { + fn from(error: webidl::WebIdlError) -> Self { + Self::WebIdl(error) + } +} + +impl From for FetchArgumentError { + fn from(error: ResolveContextUrlError) -> Self { + Self::Url(error) + } +} + +impl fmt::Display for FetchArgumentError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::WebIdl(error) => error.fmt(f), + Self::UnsupportedNoCorsMethod { method, interface } => { + write!(f, "Failed to execute 'fetch'")?; + if let Some(interface) = interface { + write!(f, " on '{interface}'")?; + } + write!(f, ": method `{method}` is unsupported in no-cors mode.") + } + Self::Url(error) => error.fmt(f), + } + } +} + +impl std::error::Error for FetchArgumentError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::WebIdl(error) => Some(error), + Self::Url(error) => Some(error), + Self::UnsupportedNoCorsMethod { .. } => None, + } + } +} diff --git a/moli-renderer-v8/src/network_host/request/init.rs b/moli-renderer-v8/src/network_host/request/init.rs index 6e24cd534e..b8e8eb4143 100644 --- a/moli-renderer-v8/src/network_host/request/init.rs +++ b/moli-renderer-v8/src/network_host/request/init.rs @@ -9,11 +9,18 @@ use std::str::FromStr; /// is consumed or any request is dispatched. pub(crate) fn convert_fetch_arguments<'s, T>( scope: &mut v8::PinScope<'s, '_>, - convert: impl FnOnce(&mut v8::PinScope<'s, '_>) -> Result, + convert: impl FnOnce(&mut v8::PinScope<'s, '_>) -> Result, ) -> Result> { let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); let mut conversion_scope = try_catch.init(); let result = convert(&mut conversion_scope); + if !conversion_scope.has_caught() + && let Err(error) = &result + { + // WebIDL's PendingException already carries an exception through V8; + // only ordinary validation failures need a new TypeError here. + error.throw(&mut conversion_scope); + } if conversion_scope.has_caught() { let exception = conversion_scope .exception() @@ -21,11 +28,7 @@ pub(crate) fn convert_fetch_arguments<'s, T>( conversion_scope.reset(); return Err(exception); } - result.map_err(|message| { - crate::util::v8_string(&conversion_scope, &message) - .map(|message| v8::Exception::type_error(&conversion_scope, message)) - .unwrap_or_else(|| v8::undefined(&conversion_scope).into()) - }) + result.map_err(|_| v8::undefined(&conversion_scope).into()) } #[derive(Debug, Clone)] @@ -135,7 +138,7 @@ pub(crate) fn parse_fetch_init<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, index: i32, -) -> Result { +) -> Result { if args.length() <= index { return Ok(ParsedFetchInit::default()); } @@ -144,31 +147,32 @@ pub(crate) fn parse_fetch_init<'s>( return Ok(ParsedFetchInit::default()); }; - let init = webidl::parse_dictionary_object::(scope, init_object) - .map_err(|error| error.to_string())?; + let init = webidl::parse_dictionary_object::(scope, init_object)?; let priority = init.priority.map(|value| value.0); let method_present = init_object .has(scope, v8str(scope, "method").into()) - .unwrap_or(false); + .ok_or_else(|| { + webidl::WebIdlError::pending_exception(webidl::Context::member("RequestInit", "method")) + })?; let method = init .method - .map(|s| normalize_request_method(&s).map_err(str::to_owned)) + .map(|s| normalize_request_method(&s).map_err(webidl::WebIdlError::custom_message)) .transpose()? .unwrap_or_else(|| "GET".to_owned()); let body_present = init_object .has(scope, v8str(scope, "body").into()) - .unwrap_or(false); + .ok_or_else(|| { + webidl::WebIdlError::pending_exception(webidl::Context::member("RequestInit", "body")) + })?; let prepared_body = webidl::property_result( scope, init_object, "body", webidl::Context::member("RequestInit", "body"), - ) - .map_err(|error| error.to_string())? + )? .map(|value| body_init(scope, value, webidl::Context::member("RequestInit", "body"))) - .transpose() - .map_err(|error| error.to_string())? + .transpose()? .flatten(); let body = prepared_body.as_ref().map(|body| body.bytes.clone()); let body_content_type = prepared_body @@ -177,12 +181,19 @@ pub(crate) fn parse_fetch_init<'s>( if body.as_ref().is_some_and(|body| !body.is_empty()) && matches!(method.as_str(), "GET" | "HEAD") { - return Err("Request with GET/HEAD method cannot have body".to_owned()); + return Err(webidl::WebIdlError::custom_message( + "Request with GET/HEAD method cannot have body", + )); } let headers_present = init_object .has(scope, v8str(scope, "headers").into()) - .unwrap_or(false); + .ok_or_else(|| { + webidl::WebIdlError::pending_exception(webidl::Context::member( + "RequestInit", + "headers", + )) + })?; let mut extra_headers = init.headers.unwrap_or_default(); append_default_body_content_type(&mut extra_headers, body_content_type.as_deref()); @@ -216,14 +227,13 @@ pub(crate) fn parse_fetch_init<'s>( pub(crate) fn request_object_credentials_mode<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, -) -> Result, String> { +) -> Result, webidl::WebIdlError> { if is_branded_request_object(scope, object) { return Ok(request_slot_string(scope, object, REQUEST_CREDENTIALS_SLOT) .and_then(|value| RequestCredentialsMode::from_str(&value).ok())); } webidl::parse_dictionary_object::(scope, object) .map(|parsed| parsed.credentials_mode.map(|value| value.0)) - .map_err(|error| error.to_string()) } pub(in crate::network_host) fn request_credentials_mode_label( diff --git a/moli-renderer-v8/src/network_host/request/input.rs b/moli-renderer-v8/src/network_host/request/input.rs index bc96c5fc05..5809c90609 100644 --- a/moli-renderer-v8/src/network_host/request/input.rs +++ b/moli-renderer-v8/src/network_host/request/input.rs @@ -350,7 +350,9 @@ pub(crate) fn try_resolve_request_constructor_url_for_scope( return Ok(input.to_owned()); } if let Some(base_url) = base_url { - return resolve_context_url(&base_url, input, None).map(|url| url.to_string()); + return resolve_context_url(&base_url, input, None) + .map(|url| url.to_string()) + .map_err(|error| error.to_string()); } if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { let host = unsafe { &mut *host_ptr }; @@ -363,12 +365,16 @@ pub(crate) fn try_resolve_request_constructor_url_for_scope( let (_, document_url, _) = effective_subresource_request_scope(scope, host, None); document_url }); - resolve_context_url(&document_url, input, None).map(|url| url.to_string()) + resolve_context_url(&document_url, input, None) + .map(|url| url.to_string()) + .map_err(|error| error.to_string()) } else if let Some(worker_url) = crate::context_bootstrap::current_worker_script_url(scope) { if worker_url.scheme() == "blob" { return Err(format!("Failed to parse URL from {input}")); } - resolve_context_url(&worker_url, input, None).map(|url| url.to_string()) + resolve_context_url(&worker_url, input, None) + .map(|url| url.to_string()) + .map_err(|error| error.to_string()) } else { Ok(input.to_owned()) } diff --git a/moli-renderer-v8/src/network_host/url_helpers.rs b/moli-renderer-v8/src/network_host/url_helpers.rs index bb8d268278..2ac8e6f5a7 100644 --- a/moli-renderer-v8/src/network_host/url_helpers.rs +++ b/moli-renderer-v8/src/network_host/url_helpers.rs @@ -1,18 +1,59 @@ +use std::fmt; + +#[derive(Debug)] +pub(crate) enum ResolveContextUrlError { + Base { + input: String, + source: url::ParseError, + }, + Url { + input: String, + source: url::ParseError, + }, +} + +impl fmt::Display for ResolveContextUrlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Base { input, source } => { + write!(f, "failed to resolve base url `{input}`: {source}") + } + Self::Url { input, source } => { + write!(f, "failed to resolve url `{input}`: {source}") + } + } + } +} + +impl std::error::Error for ResolveContextUrlError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Base { source, .. } | Self::Url { source, .. } => Some(source), + } + } +} + pub(crate) fn resolve_context_url( document_url: &url::Url, input: &str, base: Option<&str>, -) -> std::result::Result { +) -> Result { let resolved_base = match base { Some(base) => url::Url::parse(base) .or_else(|_| document_url.join(base)) - .map_err(|error| format!("failed to resolve base url `{base}`: {error}"))?, + .map_err(|source| ResolveContextUrlError::Base { + input: base.to_owned(), + source, + })?, None => document_url.clone(), }; url::Url::parse(input) .or_else(|_| resolved_base.join(input)) - .map_err(|error| format!("failed to resolve url `{input}`: {error}")) + .map_err(|source| ResolveContextUrlError::Url { + input: input.to_owned(), + source, + }) } pub(in crate::network_host) fn merge_subresource_request_headers( diff --git a/moli-renderer-v8/src/network_host/xhr/send/request.rs b/moli-renderer-v8/src/network_host/xhr/send/request.rs index 10ca609bd4..b93cb862f6 100644 --- a/moli-renderer-v8/src/network_host/xhr/send/request.rs +++ b/moli-renderer-v8/src/network_host/xhr/send/request.rs @@ -78,8 +78,8 @@ pub(super) fn prepare_xhr_send_request<'s>( } = environment; let policy_context = effective_subresource_policy_context(scope, host, owner); let network_partition_key = active_subresource_network_partition_key(host, owner); - let resolved_url = - resolve_context_url(&base_url, &url_str, None).map_err(XhrSendPrepareError::Url)?; + let resolved_url = resolve_context_url(&base_url, &url_str, None) + .map_err(|error| XhrSendPrepareError::Url(error.to_string()))?; let (request_headers, cors_preflight_request_headers) = xhr_request_headers(scope, host, xhr, prepared_body.default_content_type); let credentials_mode = diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs b/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs index b5ca6c1226..33426c8d41 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs @@ -7074,7 +7074,7 @@ async fn blob_url_revocation_uses_window_and_worker_creator_storage_keys() { } #[tokio::test] -async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming_input() { +async fn request_and_fetch_argument_errors_preserve_exceptions_without_side_effects() { run_page_vm_async_test(async move { for worker in [false, true] { let mut page_vm = test_page_vm(); @@ -7083,7 +7083,7 @@ async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming 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 member of ['method', 'headers', 'signal', 'body']) { for (const sentinel of [undefined, null, false, 0, 'sentinel', {}, new Error('sentinel')]) { const input = new Request(url, {method:'POST', body:'kept'}); const init = {}; @@ -7103,6 +7103,52 @@ async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming } } } + const rejection = async promise => { + check(promise instanceof Promise, 'fetch conversion must return a Promise'); + let rejected = false, caught; + await promise.then(() => {}, error => { rejected = true; caught = error; }); + check(rejected, 'invalid arguments must reject'); + return caught; + }; + for (const sentinel of [undefined, null, false, 0, 'sentinel', {}, new Error('sentinel')]) { + const throwSentinel = () => { throw sentinel; }; + const inits = [ + {method: {toString: throwSentinel}}, + {headers: {[Symbol.iterator]: throwSentinel}}, + ...['method', 'body', 'headers', 'signal'].map(member => new Proxy({}, { + has(target, key) { + if (key === member) throw sentinel; + return Reflect.has(target, key); + }, + })), + ]; + for (const init of inits) { + const input = new Request(url, {method: 'POST', body: 'kept'}); + check(Object.is(await rejection(fetch(input, init)), sentinel), 'nested conversion exception identity'); + check(!input.bodyUsed, 'nested conversion failure consumed input body'); + } + check(Object.is(await rejection(fetch({toString: throwSentinel})), sentinel), 'URL conversion exception identity'); + } + const interfaceName = typeof document === 'undefined' ? 'DedicatedWorkerGlobalScope' : 'Window'; + const noCorsInterface = typeof document === 'undefined' ? " on 'DedicatedWorkerGlobalScope'" : ''; + for (const [init, message] of [ + [{method: 'TRACE'}, 'Request method is forbidden'], + [{method: 'GET', body: 'invalid'}, 'Request with GET/HEAD method cannot have body'], + [{mode: 'invalid'}, 'RequestInit: mode is not a valid enum value of type RequestMode'], + [{signal: false}, `Failed to execute 'fetch' on '${interfaceName}': signal must be an AbortSignal.`], + [{signal: {}}, `Failed to execute 'fetch' on '${interfaceName}': signal must be an AbortSignal.`], + [{mode: 'no-cors', method: 'PATCH'}, `Failed to execute 'fetch'${noCorsInterface}: method \`PATCH\` is unsupported in no-cors mode.`], + ]) { + const input = new Request(url, {method: 'POST', body: 'kept'}); + const error = await rejection(fetch(input, init)); + check(error instanceof TypeError, 'validation must reject with a TypeError'); + check(error.message === message, 'validation message: ' + error.message); + check(!input.bodyUsed, 'validation failure consumed input body'); + } + const missing = await rejection(fetch()); + check(missing instanceof TypeError && missing.message === 'fetch: Argument 1 is required', 'missing argument validation'); + const badUrl = await rejection(fetch('http://[')); + check(badUrl instanceof TypeError && badUrl.message.startsWith('failed to resolve url `http://[`:'), 'invalid URL validation'); if (typeof document !== 'undefined') { const frame = document.createElement('iframe'); document.body.appendChild(frame); @@ -7113,6 +7159,10 @@ async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming let caught; await promise.catch(error => { caught = error; }); check(caught === sentinel, 'cross-realm exception identity'); + const invalidSignal = other.fetch.call(window, url, {signal: false}); + check(invalidSignal instanceof other.Promise, 'validation rejection must belong to function realm'); + await invalidSignal.catch(error => { caught = error; }); + check(caught instanceof other.TypeError, 'validation TypeError must belong to function realm'); frame.remove(); } return 'ok'; diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 1d10ac0d73..12d92f0aca 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -1,5 +1,5 @@ use super::*; -use crate::network_host::convert_fetch_arguments; +use crate::network_host::{FetchArgumentError, convert_fetch_arguments}; use crate::service_worker_runtime::{ ServiceWorkerClientId, ServiceWorkerDirectFetchResult, ServiceWorkerFetchDispatch, ServiceWorkerFetchRequest, ServiceWorkerFetchRequestMetadata, ServiceWorkerRequestDestination, @@ -1771,21 +1771,19 @@ pub(in crate::worker) fn make_rejected_promise_with_value<'s>( pub(in crate::worker) fn validate_worker_fetch_signal<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, -) -> Result>, String> { +) -> Result>, webidl::WebIdlError> { if value.is_null_or_undefined() { return Ok(None); } let Ok(signal) = v8::Local::::try_from(value) else { - return Err( - "Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal." - .to_owned(), - ); + return Err(webidl::WebIdlError::custom_message( + "Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal.", + )); }; if worker_abort_signal_id(scope, signal).is_none() { - return Err( - "Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal." - .to_owned(), - ); + return Err(webidl::WebIdlError::custom_message( + "Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal.", + )); } Ok(Some(signal)) } @@ -1794,23 +1792,25 @@ pub(in crate::worker) fn worker_fetch_signal_option<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, request_like: Option>, -) -> Result>, String> { +) -> Result>, webidl::WebIdlError> { let signal_key = v8str(scope, "signal"); if args.length() > 1 { let init_arg = args.get(1); if !init_arg.is_null_or_undefined() && let Ok(init) = v8::Local::::try_from(init_arg) - && init - .has(scope, signal_key.into()) - .ok_or("Failed to read RequestInit.signal")? + && init.has(scope, signal_key.into()).ok_or_else(|| { + webidl::WebIdlError::pending_exception(webidl::Context::member( + "RequestInit", + "signal", + )) + })? { let signal = webidl::property_result( scope, init, "signal", webidl::Context::member("RequestInit", "signal"), - ) - .map_err(|error| error.to_string())? + )? .unwrap_or_else(|| v8::undefined(scope).into()); return validate_worker_fetch_signal(scope, signal); } @@ -1821,8 +1821,7 @@ pub(in crate::worker) fn worker_fetch_signal_option<'s>( request_like, "signal", webidl::Context::member("Request", "signal"), - ) - .map_err(|error| error.to_string())? + )? { return validate_worker_fetch_signal(scope, signal); } @@ -1846,17 +1845,16 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, base_url: &Url, -) -> Result, String> { +) -> Result, FetchArgumentError> { if args.length() < 1 { return Err( - webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1)) - .to_string(), + webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1)).into(), ); } let arg0 = args.get(0); let mut request_like = None; let mut consumes_request_body = false; - let inherited = request_input_snapshot(scope, arg0).map_err(|error| error.to_string())?; + let inherited = request_input_snapshot(scope, arg0)?; let ( url_input, method, @@ -1938,8 +1936,7 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>( arg0, webidl::Context::argument("fetch", 1), ) - .map(String::from) - .map_err(|error| error.to_string())?; + .map(String::from)?; let init = parse_fetch_init(scope, args, 1)?; let request_mode = init.request_mode.unwrap_or(moli_fetch::RequestMode::Cors); validate_worker_no_cors_method(request_mode, &init.method)?; @@ -1993,11 +1990,12 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>( pub(in crate::worker) fn validate_worker_no_cors_method( request_mode: RequestMode, method: &str, -) -> Result<(), String> { +) -> Result<(), FetchArgumentError> { if request_mode == RequestMode::NoCors && !moli_fetch::is_cors_safelisted_method(method) { - return Err(format!( - "Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': method `{method}` is unsupported in no-cors mode." - )); + return Err(FetchArgumentError::UnsupportedNoCorsMethod { + method: method.to_owned(), + interface: Some("DedicatedWorkerGlobalScope"), + }); } Ok(()) } diff --git a/moli-renderer-v8/src/worker/global_scope/xhr.rs b/moli-renderer-v8/src/worker/global_scope/xhr.rs index eb5f7b1997..b6b7a6b764 100644 --- a/moli-renderer-v8/src/worker/global_scope/xhr.rs +++ b/moli-renderer-v8/src/worker/global_scope/xhr.rs @@ -1182,7 +1182,7 @@ pub(in crate::worker) fn prepare_worker_xhr_send_request<'s>( ) })?; let resolved_url = resolve_context_url(&document_url, &url_str, None) - .map_err(WorkerXhrSendPrepareError::Request)?; + .map_err(|error| WorkerXhrSendPrepareError::Request(error.to_string()))?; let request_headers = xhr_author_request_headers(scope, xhr, prepared_body.default_content_type); let credentials_mode =