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 =