diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index c0f40fdf5..a043a728f 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -461,9 +461,8 @@ use self::window_runtime::global_caches_getter_callback; pub(crate) use self::window_runtime::install_child_window_own_methods; pub(crate) use self::window_template::install_window_own_template_bindings; pub(crate) use self::worker_host::{ - dispatch_worker_error_event_with_error, dispatch_worker_error_event_with_kind, - dispatch_worker_event, flush_pending_worker_messages_for_listener, - worker_has_message_delivery_listener, + dispatch_worker_error_event_with_kind, dispatch_worker_event, + flush_pending_worker_messages_for_listener, worker_has_message_delivery_listener, }; pub(crate) use self::worker_location_runtime::install_worker_location_runtime_state; pub(super) use super::{ diff --git a/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs b/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs index de48fd509..4c24c7d4e 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs @@ -1,7 +1,7 @@ use super::{ CHILD_BROWSING_CONTEXT_HANDLE_SLOT, MessagePortRealmBinding, ensure_message_port_wrapper_for_id_in_realm, - events::{clear_event_dispatch_fields, set_event_dispatch_fields}, + events::{clear_event_dispatch_fields, construct_original_event, set_event_dispatch_fields}, invoke_simple_event_listener, navigation_serialize::{ current_document_content_security_policies, current_document_referrer_policy, @@ -87,22 +87,6 @@ struct SharedWorkerObjectDeclaration<'scope> { port: v8::Local<'scope, v8::Object>, } -#[derive(WebApiObject)] -#[webapi(plain)] -struct SharedWorkerHostEventInitDeclaration { - #[webapi(data_property, enumerable)] - cancelable: bool, -} - -#[derive(WebApiObject)] -#[webapi(interface = web_api_interfaces::Event, prototype = "Object")] -struct SharedWorkerHostEventFallbackDeclaration { - #[webapi(data_property, enumerable)] - r#type: String, - #[webapi(data_property, enumerable)] - cancelable: bool, -} - #[derive(WebApiObject)] #[webapi(plain)] struct SharedWorkerHostErrorEventInitDeclaration<'scope> { @@ -137,16 +121,6 @@ struct SharedWorkerHostErrorEventFallbackDeclaration<'scope, 'text> { error: v8::Local<'scope, v8::Value>, } -#[derive(WebApiObject)] -#[webapi(plain, scope_lifetime = 'scope, data_properties, enumerable)] -struct SharedWorkerHostErrorEventDetailsDeclaration<'scope, 'text> { - message: &'text str, - filename: &'text str, - lineno: u32, - colno: u32, - error: v8::Local<'scope, v8::Value>, -} - struct SharedWorkerOptions { name: String, script_kind: WorkerScriptKind, @@ -785,15 +759,16 @@ fn dispatch_shared_worker_error_event<'s>( event_kind: crate::worker::WorkerParentErrorEventKind, ) -> bool { let event = match event_kind { + // Fetch/parse failures fire a plain Event with the default flags. crate::worker::WorkerParentErrorEventKind::Event => { - let event = new_event(scope, "error", true); - set_error_event_details(scope, event, message, filename, lineno, colno, error); - event - } - crate::worker::WorkerParentErrorEventKind::ErrorEvent => { - new_error_event(scope, message, filename, lineno, colno, error) + construct_original_event(scope, "error") } + crate::worker::WorkerParentErrorEventKind::ErrorEvent => Some(new_error_event( + scope, message, filename, lineno, colno, error, + )), }; + let Some(event) = event else { return false }; + crate::context_bootstrap::mark_event_trusted(scope, event); set_event_dispatch_fields(scope, worker, event); let listeners = simple_object_event_listeners_snapshot( @@ -815,7 +790,8 @@ fn dispatch_shared_worker_error_event<'s>( &[event.into()], event, ); - if listener.handler_slot.as_deref() == Some(SHARED_WORKER_ONERROR_SLOT) + if event_kind == crate::worker::WorkerParentErrorEventKind::ErrorEvent + && listener.handler_slot.as_deref() == Some(SHARED_WORKER_ONERROR_SLOT) && let Some(returned) = callback_result && v8::Local::new(scope, &returned).boolean_value(scope) { @@ -845,35 +821,6 @@ fn dispatch_shared_worker_error_event<'s>( dispatched } -fn new_event<'s>( - scope: &mut v8::PinScope<'s, '_>, - event_type: &str, - cancelable: bool, -) -> v8::Local<'s, v8::Object> { - let global = scope.get_current_context().global(scope); - if let Some(event_ctor) = global - .get(scope, v8str(scope, "Event").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - { - let init = SharedWorkerHostEventInitDeclaration::new(cancelable) - .bind(scope) - .expect("SharedWorker host Event init declaration should bind"); - if let Some(event) = event_ctor.new_instance( - scope, - &[ - v8::String::new(scope, event_type).unwrap().into(), - init.into(), - ], - ) { - return event; - } - } - - SharedWorkerHostEventFallbackDeclaration::new(event_type.to_owned(), cancelable) - .bind(scope) - .expect("SharedWorker host Event fallback declaration should bind") -} - fn new_error_event<'s>( scope: &mut v8::PinScope<'s, '_>, message: &str, @@ -911,17 +858,3 @@ fn new_error_event<'s>( .bind(scope) .expect("SharedWorker host ErrorEvent fallback declaration should bind") } - -fn set_error_event_details<'s>( - scope: &mut v8::PinScope<'s, '_>, - event: v8::Local<'s, v8::Object>, - message: &str, - filename: &str, - lineno: u32, - colno: u32, - error: v8::Local<'s, v8::Value>, -) { - SharedWorkerHostErrorEventDetailsDeclaration::new(message, filename, lineno, colno, error) - .initialize(scope, event) - .expect("SharedWorker host ErrorEvent details declaration should initialize"); -} diff --git a/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs b/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs index e3b2205d3..bfe217d2b 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs @@ -488,7 +488,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( lineno: 0, colno: 0, event_kind: crate::worker::WorkerParentErrorEventKind::Event, - phase: crate::worker::WorkerErrorPhase::Runtime, + phase: crate::worker::WorkerErrorPhase::Bootstrap, source: crate::worker::WorkerErrorSource::Runtime, }), }, diff --git a/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs index 5523fa31f..6206abf19 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs @@ -6,7 +6,7 @@ use moli_webapi_declare::WebApiObject; use super::{WORKER_LISTENERS_SLOT, WORKER_ONERROR_SLOT}; use crate::context_bootstrap::{ dispatch_simple_event_target_event, - events::{clear_event_dispatch_fields, set_event_dispatch_fields}, + events::{clear_event_dispatch_fields, construct_original_event, set_event_dispatch_fields}, invoke_simple_event_listener, simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, }; @@ -14,22 +14,6 @@ use crate::structured_clone::V8StructuredClonePayload; use crate::util::v8str; use crate::worker::{WorkerParentErrorEventKind, WorkerToParentMessage}; -#[derive(WebApiObject)] -#[webapi(plain)] -struct WorkerHostEventInitDeclaration { - #[webapi(data_property, enumerable)] - cancelable: bool, -} - -#[derive(WebApiObject)] -#[webapi(interface = web_api_interfaces::Event, prototype = "Object")] -struct WorkerHostEventFallbackDeclaration { - #[webapi(data_property, enumerable)] - r#type: String, - #[webapi(data_property, enumerable)] - cancelable: bool, -} - #[derive(WebApiObject)] #[webapi(plain)] struct WorkerHostMessageEventInitDeclaration<'scope> { @@ -84,16 +68,6 @@ struct WorkerHostErrorEventFallbackDeclaration<'scope, 'text> { error: v8::Local<'scope, v8::Value>, } -#[derive(WebApiObject)] -#[webapi(plain, scope_lifetime = 'scope, data_properties, enumerable)] -struct WorkerHostErrorEventDetailsDeclaration<'scope, 'text> { - message: &'text str, - filename: &'text str, - lineno: u32, - colno: u32, - error: v8::Local<'scope, v8::Value>, -} - pub(crate) fn dispatch_worker_event<'s>( scope: &mut v8::PinScope<'s, '_>, worker: v8::Local<'s, v8::Object>, @@ -255,27 +229,6 @@ pub(crate) fn flush_pending_worker_messages_for_listener<'s>( let _ = (scope, worker); } -pub(crate) fn dispatch_worker_error_event_with_error<'s>( - scope: &mut v8::PinScope<'s, '_>, - worker: v8::Local<'s, v8::Object>, - message: &str, - filename: &str, - lineno: u32, - colno: u32, - error: v8::Local<'s, v8::Value>, -) -> bool { - dispatch_worker_error_event_with_kind( - scope, - worker, - message, - filename, - lineno, - colno, - error, - WorkerParentErrorEventKind::ErrorEvent, - ) -} - pub(crate) fn dispatch_worker_error_event_with_kind<'s>( scope: &mut v8::PinScope<'s, '_>, worker: v8::Local<'s, v8::Object>, @@ -287,15 +240,14 @@ pub(crate) fn dispatch_worker_error_event_with_kind<'s>( event_kind: WorkerParentErrorEventKind, ) -> bool { let event = match event_kind { - WorkerParentErrorEventKind::Event => { - let event = new_event(scope, "error", true); - set_error_event_details(scope, event, message, filename, lineno, colno, error); - event - } - WorkerParentErrorEventKind::ErrorEvent => { - new_error_event(scope, message, filename, lineno, colno, error) - } + // Fetch/parse failures fire a plain Event with the default flags. + WorkerParentErrorEventKind::Event => construct_original_event(scope, "error"), + WorkerParentErrorEventKind::ErrorEvent => Some(new_error_event( + scope, message, filename, lineno, colno, error, + )), }; + let Some(event) = event else { return false }; + crate::context_bootstrap::mark_event_trusted(scope, event); set_event_dispatch_fields(scope, worker, event); let listeners = @@ -311,7 +263,8 @@ pub(crate) fn dispatch_worker_error_event_with_kind<'s>( &[event.into()], event, ); - if listener.handler_slot.as_deref() == Some(WORKER_ONERROR_SLOT) + if event_kind == WorkerParentErrorEventKind::ErrorEvent + && listener.handler_slot.as_deref() == Some(WORKER_ONERROR_SLOT) && let Some(returned) = callback_result && v8::Local::new(scope, &returned).boolean_value(scope) { @@ -351,35 +304,6 @@ pub(crate) fn dispatch_worker_error_event_with_kind<'s>( !default_prevented } -fn new_event<'s>( - scope: &mut v8::PinScope<'s, '_>, - event_type: &str, - cancelable: bool, -) -> v8::Local<'s, v8::Object> { - let global = scope.get_current_context().global(scope); - if let Some(event_ctor) = global - .get(scope, v8str(scope, "Event").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - { - let init = WorkerHostEventInitDeclaration::new(cancelable) - .bind(scope) - .expect("worker host Event init declaration should bind"); - if let Some(event) = event_ctor.new_instance( - scope, - &[ - v8::String::new(scope, event_type).unwrap().into(), - init.into(), - ], - ) { - return event; - } - } - - WorkerHostEventFallbackDeclaration::new(event_type.to_owned(), cancelable) - .bind(scope) - .expect("worker host Event fallback declaration should bind") -} - fn new_message_event<'s>( scope: &mut v8::PinScope<'s, '_>, event_type: &str, @@ -441,17 +365,3 @@ fn new_error_event<'s>( .bind(scope) .expect("worker host ErrorEvent fallback declaration should bind") } - -fn set_error_event_details<'s>( - scope: &mut v8::PinScope<'s, '_>, - event: v8::Local<'s, v8::Object>, - message: &str, - filename: &str, - lineno: u32, - colno: u32, - error: v8::Local<'s, v8::Value>, -) { - WorkerHostErrorEventDetailsDeclaration::new(message, filename, lineno, colno, error) - .initialize(scope, event) - .expect("worker host ErrorEvent details declaration should initialize"); -} diff --git a/moli-renderer-v8/src/context_bootstrap/worker_host/mod.rs b/moli-renderer-v8/src/context_bootstrap/worker_host/mod.rs index 94f0e1180..47989de2c 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/mod.rs @@ -35,9 +35,8 @@ pub(in crate::context_bootstrap) use constructor::{ pub(super) use constructor::worker_constructor_callback; pub(crate) use dispatch::{ - dispatch_worker_error_event_with_error, dispatch_worker_error_event_with_kind, - dispatch_worker_event, flush_pending_worker_messages_for_listener, - worker_has_message_delivery_listener, + dispatch_worker_error_event_with_kind, dispatch_worker_event, + flush_pending_worker_messages_for_listener, worker_has_message_delivery_listener, }; pub(super) use methods::{worker_post_message_callback, worker_terminate_callback}; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/dedicated_worker_client_event.rs b/moli-renderer-v8/src/runtime/page_vm/tests/dedicated_worker_client_event.rs index d48bfda15..c3a926c3b 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/dedicated_worker_client_event.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/dedicated_worker_client_event.rs @@ -266,7 +266,7 @@ __dedicatedWorkerErrorPhaseWorker.onerror = event => { page_vm .vm_mut() .eval("__dedicatedWorkerErrorPhases.join('|')")?, - "worker:Event:bootstrap syntax:true", + "worker:Event:undefined:false", "initial script errors must stop after the Worker error event" ); @@ -296,7 +296,7 @@ __dedicatedWorkerErrorPhaseWorker.onerror = event => { page_vm .vm_mut() .eval("__dedicatedWorkerErrorPhases.join('|')")?, - "worker:Event:bootstrap syntax:true|worker:ErrorEvent:runtime boom:true|window:ErrorEvent:runtime boom", + "worker:Event:undefined:false|worker:ErrorEvent:runtime boom:true|window:ErrorEvent:runtime boom", "uncanceled runtime errors must retain their owning Window propagation" ); @@ -306,6 +306,87 @@ __dedicatedWorkerErrorPhaseWorker.onerror = event => { .expect("DedicatedWorker bootstrap/runtime error propagation test should run"); } +#[tokio::test(flavor = "current_thread")] +async fn dedicated_worker_bootstrap_error_uses_intrinsic_event_without_author_hooks() { + run_page_vm_async_test(async move { + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); + let (mut page_vm, _resource_source, _owner_wake_rx) = + page_vm_with_bound_task_sources_and_owner_wake( + &loader, + Url::parse("https://worker-bootstrap-event.test/").unwrap(), + ); + page_vm.vm_mut().eval( + r#" +globalThis.__bootstrapEventWorker = new Worker("data:text/javascript,onmessage = () => {}"); +(() => { + const originalEvent = Event; + const originalEventDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'Event'); + const flags = ['bubbles', 'cancelable', 'composed']; + const originalFlags = flags.map(name => Object.getOwnPropertyDescriptor(Object.prototype, name)); + let reads = 0; + const poison = {configurable: true, get() { ++reads; throw new Error('author event hook'); }}; + globalThis.__bootstrapEventFailures = 'pending'; + __bootstrapEventWorker.onerror = function(event) { + const failures = []; + try { + if (Object.getPrototypeOf(event) !== originalEvent.prototype) failures.push('wrong Event prototype'); + if (event.type !== 'error' || event.isTrusted !== true) failures.push('wrong type or trust'); + if (event.target !== __bootstrapEventWorker || event.currentTarget !== this || this !== __bootstrapEventWorker) failures.push('wrong dispatch target'); + if (arguments.length !== 1) failures.push('wrong callback arguments'); + for (const name of flags) if (event[name] !== false) failures.push(name); + for (const name of ['message', 'filename', 'lineno', 'colno', 'error']) { + if (name in event) failures.push('unexpected ' + name); + } + if (event.defaultPrevented) failures.push('initially canceled'); + event.preventDefault(); + if (event.defaultPrevented) failures.push('cancelable bootstrap event'); + if (reads !== 0) failures.push('called author event hooks'); + globalThis.__bootstrapEventFailures = JSON.stringify(failures); + } finally { + Object.defineProperty(globalThis, 'Event', originalEventDescriptor); + flags.forEach((name, index) => { + if (originalFlags[index]) Object.defineProperty(Object.prototype, name, originalFlags[index]); + else delete Object.prototype[name]; + }); + } + }; + Object.defineProperty(globalThis, 'Event', poison); + for (const name of flags) Object.defineProperty(Object.prototype, name, poison); +})() +"#, + )?; + let (_worker_id, producer) = page_vm + .vm() + .only_dedicated_worker_client_event_producer_for_test()?; + producer + .send(RendererDedicatedWorkerClientEvent::Message( + RendererDedicatedWorkerMessageEvent::Error { + message: "bootstrap parse error".to_owned(), + filename: "https://worker-bootstrap-event.test/broken.js".to_owned(), + lineno: 3, + colno: 5, + event_kind: WorkerParentErrorEventKind::Event, + phase: WorkerErrorPhase::Bootstrap, + source: WorkerErrorSource::InitialScriptEvaluation, + }, + )) + .expect("bootstrap error should enter the typed Worker source"); + assert!( + page_vm + .run_exact_selected_page_task_for_test( + PageSelectedTaskTestSelector::DedicatedWorkerClientEvent, + &loader, + ) + .await? + ); + assert_eq!(page_vm.vm_mut().eval("__bootstrapEventFailures")?, "[]"); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("Worker bootstrap errors should use the intrinsic Event"); +} + #[tokio::test(flavor = "current_thread")] async fn dedicated_worker_relay_terminal_waits_for_both_selected_source_fifos() { run_page_vm_async_test(async move { 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 4736a575b..b64f376ca 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 @@ -6337,7 +6337,7 @@ async fn worker_url_fetch_failure_dispatches_error_event() { let mut page_vm = test_page_vm_with_document_url(document_url); let local_executor = page_vm.local_executor.clone(); - let error_message = local_executor + let error_event = local_executor .run(async move { page_vm.vm_mut().eval( r#" @@ -6346,7 +6346,13 @@ async fn worker_url_fetch_failure_dispatches_error_event() { globalThis.__workerDone = false; const worker = new Worker("/missing-worker.js"); worker.onerror = (event) => { - globalThis.__workerError = event.message; + globalThis.__workerError = [ + event.type, + Object.getPrototypeOf(event) === Event.prototype, + event.target === worker, + event.bubbles, event.cancelable, event.composed, event.isTrusted, + ['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event) + ]; globalThis.__workerDone = true; }; })() @@ -6358,7 +6364,7 @@ async fn worker_url_fetch_failure_dispatches_error_event() { "worker url load failure should dispatch an error event", ) .await?; - page_vm.vm_mut().eval("String(globalThis.__workerError)") + page_vm.vm_mut().eval("JSON.stringify(globalThis.__workerError)") }) .await .expect("worker url failure test should run on owner lane"); @@ -6366,11 +6372,9 @@ async fn worker_url_fetch_failure_dispatches_error_event() { server .await .expect("worker script failure server should finish"); - assert!( - error_message.contains("HTTP request") - && error_message.contains("404") - && error_message.contains("/missing-worker.js"), - "unexpected worker load error: {error_message}" + assert_eq!( + error_event, + r#"["error",true,true,false,false,false,true,false]"# ); }) .await; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/shared_worker_client_event.rs b/moli-renderer-v8/src/runtime/page_vm/tests/shared_worker_client_event.rs index b93b810e7..38333c07b 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/shared_worker_client_event.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/shared_worker_client_event.rs @@ -170,6 +170,95 @@ async fn shared_worker_error_body_leaves_reactions_for_selected_completion() { .expect("SharedWorker current-owner event should run through its typed executor"); } +#[tokio::test(flavor = "current_thread")] +async fn shared_worker_bootstrap_error_uses_intrinsic_event_without_author_hooks() { + run_page_vm_async_test(async move { + for (setup, remaining_clients) in [ + (r#" +globalThis.__bootstrapEventWorker = new SharedWorker( + "data:text/javascript," + encodeURIComponent("function ("), + "intrinsic-bootstrap-event" +); +"#, 0), + (r#" +const url = "data:text/javascript,onconnect = () => {}"; +globalThis.__runningSharedWorker = new SharedWorker(url, "intrinsic-connection-event"); +globalThis.__bootstrapEventWorker = new SharedWorker(url, { + name: "intrinsic-connection-event", type: "module" +}); +"#, 1), + ] { + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); + let (mut page_vm, _resource_source, mut page_wake_rx) = + page_vm_with_bound_task_sources_and_owner_wake( + &loader, + Url::parse("https://shared-worker-bootstrap-event.test/").unwrap(), + ); + let mut shared_worker_wake_rx = install_shared_worker_service_wake(&page_vm); + page_vm.vm_mut().eval(setup)?; + page_vm.vm_mut().eval( + r#" +(() => { + const originalEvent = Event; + const originalEventDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'Event'); + const flags = ['bubbles', 'cancelable', 'composed']; + const originalFlags = flags.map(name => Object.getOwnPropertyDescriptor(Object.prototype, name)); + let reads = 0; + const poison = {configurable: true, get() { ++reads; throw new Error('author event hook'); }}; + globalThis.__bootstrapEventFailures = 'pending'; + __bootstrapEventWorker.onerror = function(event) { + const failures = []; + try { + if (Object.getPrototypeOf(event) !== originalEvent.prototype) failures.push('wrong Event prototype'); + if (event.type !== 'error' || event.isTrusted !== true) failures.push('wrong type or trust'); + if (event.target !== __bootstrapEventWorker || event.currentTarget !== this || this !== __bootstrapEventWorker) failures.push('wrong dispatch target'); + if (arguments.length !== 1) failures.push('wrong callback arguments'); + for (const name of flags) if (event[name] !== false) failures.push(name); + for (const name of ['message', 'filename', 'lineno', 'colno', 'error']) { + if (name in event) failures.push('unexpected ' + name); + } + if (event.defaultPrevented) failures.push('initially canceled'); + event.preventDefault(); + if (event.defaultPrevented) failures.push('cancelable bootstrap event'); + if (reads !== 0) failures.push('called author event hooks'); + globalThis.__bootstrapEventFailures = JSON.stringify(failures); + } finally { + Object.defineProperty(globalThis, 'Event', originalEventDescriptor); + flags.forEach((name, index) => { + if (originalFlags[index]) Object.defineProperty(Object.prototype, name, originalFlags[index]); + else delete Object.prototype[name]; + }); + } + }; + Object.defineProperty(globalThis, 'Event', poison); + for (const name of flags) Object.defineProperty(Object.prototype, name, poison); +})() + "#, + )?; + wait_for_shared_worker_client_event( + &mut page_vm, + &mut shared_worker_wake_rx, + &mut page_wake_rx, + ) + .await?; + assert!( + page_vm + .run_exact_selected_page_task_for_test( + PageSelectedTaskTestSelector::SharedWorkerClientEvent, + &loader, + ) + .await? + ); + assert_eq!(page_vm.vm_mut().eval("__bootstrapEventFailures")?, "[]"); + assert_eq!(page_vm.vm().shared_worker_client_count_for_test(), remaining_clients); + } + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("SharedWorker bootstrap errors should use the intrinsic Event"); +} + #[tokio::test(flavor = "current_thread")] async fn shared_worker_nonterminal_error_runs_through_selected_completion_and_retains_endpoint() { run_page_vm_async_test(async move { diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs index d5d2e0f11..6ba6ca080 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs @@ -2914,13 +2914,9 @@ async fn worker_pending_activity_diagnostics_split_loading_and_running_worker_is #[tokio::test] async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports() { run_page_vm_async_test(async move { - let (dependency_base_url, dependency_server) = spawn_path_response_http_server(vec![( - "/dependency.js", - "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *", - "export const value = 'unexpected';".to_owned(), - Duration::ZERO, - )]) - .await; + let (dependency_base_url, mut dependency_request, dependency_server) = + spawn_shared_worker_script_capture_http_server("export const value = 'unexpected';") + .await; let dependency_url = format!("{dependency_base_url}/dependency.js"); let worker_source = format!( r#" @@ -2962,7 +2958,13 @@ async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports }; worker.onerror = event => { event.preventDefault(); - globalThis.__moduleWorkerCspResult = "error:" + event.message; + globalThis.__moduleWorkerCspResult = JSON.stringify([ + event.type, + Object.getPrototypeOf(event) === Event.prototype, + event.bubbles, event.cancelable, event.composed, event.isTrusted, + event.defaultPrevented, + ['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event) + ]); globalThis.__moduleWorkerCspDone = true; }; })() @@ -2977,9 +2979,10 @@ async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports let result = page_vm .vm_mut() .eval("globalThis.__moduleWorkerCspResult")?; - assert!( - result.starts_with("error:") && result.contains("Content Security Policy"), - "static module import should be blocked by creator worker-src: {result:?}" + assert_eq!( + result, + r#"["error",true,false,false,false,true,false,false]"#, + "blocked static import should fire a bootstrap Event before evaluation" ); anyhow::Ok(()) }) @@ -2988,6 +2991,10 @@ async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports server .await .expect("external module worker CSP server should finish"); + assert!( + matches!(dependency_request.try_recv(), Err(tokio::sync::oneshot::error::TryRecvError::Empty)), + "creator CSP must block the dependency before any HTTP request is sent" + ); dependency_server.abort(); }) .await; @@ -3935,7 +3942,6 @@ async fn worker_script_load_failure_does_not_dispatch_window_error() { )]) .await; let document_url = Url::parse(&format!("{base_url}/page.html")).expect("document url"); - let missing_worker_url = format!("{base_url}/does-not-exist.js"); let mut page_vm = test_page_vm_with_document_url(document_url); let local_executor = page_vm.local_executor.clone(); @@ -3952,7 +3958,17 @@ async fn worker_script_load_failure_does_not_dispatch_window_error() { }); const worker = new Worker("/does-not-exist.js"); worker.onerror = event => { - globalThis.__missingWorkerEvents.push("worker:" + event.message); + event.preventDefault(); + globalThis.__missingWorkerEvents.push({ + target: "worker", + type: event.type, + constructor: event.constructor.name, + trusted: event.isTrusted, + cancelable: event.cancelable, + defaultPrevented: event.defaultPrevented, + hasErrorDetails: ["message", "filename", "lineno", "colno", "error"] + .some(name => name in event) + }); globalThis.__missingWorkerDone = true; }; })() @@ -3973,9 +3989,7 @@ async fn worker_script_load_failure_does_not_dispatch_window_error() { page_vm .vm_mut() .eval("JSON.stringify(globalThis.__missingWorkerEvents)")?, - format!( - r#"["worker:HTTP request `{missing_worker_url}` returned 404 Not Found"]"# - ) + r#"[{"target":"worker","type":"error","constructor":"Event","trusted":true,"cancelable":false,"defaultPrevented":false,"hasErrorDetails":false}]"# ); anyhow::Ok(()) }) @@ -4006,7 +4020,10 @@ async fn shared_worker_rejects_cross_origin_redirected_script() { globalThis.__sharedWorkerRedirectDone = false; const worker = new SharedWorker("/redirect-source.js", "cross-origin-redirect-script"); worker.onerror = (event) => { - globalThis.__sharedWorkerRedirectOutcome = "error:" + event.message; + globalThis.__sharedWorkerRedirectOutcome = JSON.stringify([ + event.type, event.constructor.name, event.cancelable, + "message" in event, event.isTrusted + ]); globalThis.__sharedWorkerRedirectDone = true; }; worker.port.onmessage = (event) => { @@ -4024,9 +4041,10 @@ async fn shared_worker_rejects_cross_origin_redirected_script() { ) .await?; let outcome = page_vm.vm_mut().eval("globalThis.__sharedWorkerRedirectOutcome")?; - assert!( - outcome.starts_with("error:"), - "redirected cross-origin script must not execute, got {outcome:?}" + assert_eq!( + outcome, + r#"["error","Event",false,false,true]"#, + "redirected cross-origin script must fail with a plain Event" ); anyhow::Ok(()) }) @@ -4933,7 +4951,7 @@ async fn shared_worker_terminal_error_forgets_page_client_wrapper_tracking() { page_vm .vm_mut() .eval("JSON.stringify(globalThis.__sharedWorkerTerminalErrorRecord)")?, - r#"{"type":"error","cancelable":true,"hasMessage":true}"# + r#"{"type":"error","cancelable":false,"hasMessage":false}"# ); wait_for_shared_worker_client_count( &mut page_vm, diff --git a/moli-renderer-v8/src/runtime/tests.rs b/moli-renderer-v8/src/runtime/tests.rs index 46f461239..e3688d909 100644 --- a/moli-renderer-v8/src/runtime/tests.rs +++ b/moli-renderer-v8/src/runtime/tests.rs @@ -5095,8 +5095,9 @@ async fn dedicated_worker_script_load_failure_does_not_dispatch_window_error() { globalThis.__lm_worker_script_error_events.push([ "worker", event.type, - String(event.message).includes("HTTP request"), - String(event.filename).endsWith("/does-not-exist.js") + Object.getPrototypeOf(event) === Event.prototype, + event.bubbles, event.cancelable, event.composed, event.isTrusted, + ['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event) ].join(":")); }; return "installed"; @@ -5129,7 +5130,9 @@ async fn dedicated_worker_script_load_failure_does_not_dispatch_window_error() { .expect("worker script load failure events should evaluate"); assert_eq!( renderer_json_value(events), - Some(serde_json::json!("[\"worker:error:true:true\"]")), + Some(serde_json::json!( + "[\"worker:error:true:false:false:false:true:false\"]" + )), "worker script load failure must not bubble to window.onerror" ); diff --git a/moli-renderer-v8/src/script_vm/dedicated_worker_error_dispatch.rs b/moli-renderer-v8/src/script_vm/dedicated_worker_error_dispatch.rs index 76ba2977a..068e310b6 100644 --- a/moli-renderer-v8/src/script_vm/dedicated_worker_error_dispatch.rs +++ b/moli-renderer-v8/src/script_vm/dedicated_worker_error_dispatch.rs @@ -1,7 +1,7 @@ //! DedicatedWorker error-event dispatch and propagation. //! //! This is deliberately separate from ordinary Worker message dispatch. -//! `dispatch_worker_error_event_with_*` retains the established inner +//! `dispatch_worker_error_event_with_kind` retains the established inner //! checkpoint used to settle listener cancellation before deciding whether an //! uncanceled Worker error propagates to the owning Window. That semantic //! checkpoint is not the HTML task-end checkpoint: the selected Page-task @@ -16,7 +16,7 @@ pub(super) fn dispatch_script_load_failure<'s>( script_url: &str, ) { let worker_error = v8::null(scope).into(); - crate::context_bootstrap::dispatch_worker_error_event_with_error( + crate::context_bootstrap::dispatch_worker_error_event_with_kind( scope, worker, error_message, @@ -24,6 +24,7 @@ pub(super) fn dispatch_script_load_failure<'s>( 0, 0, worker_error, + crate::worker::WorkerParentErrorEventKind::Event, ); } diff --git a/moli-renderer-v8/src/shared_worker_runtime/connection.rs b/moli-renderer-v8/src/shared_worker_runtime/connection.rs index b64790380..4490d7a02 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/connection.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/connection.rs @@ -67,7 +67,7 @@ fn connect_with_runtime_service( client.send_error( "Failed to connect SharedWorker: loading host is unavailable.", params.key.script_url(), - WorkerParentErrorEventKind::ErrorEvent, + WorkerParentErrorEventKind::Event, ); client.close_ports(); runtime_service.remove_client(client_id); @@ -88,7 +88,7 @@ fn connect_with_runtime_service( client.send_error( shared_worker_compatibility_error_message(&error), params.key.script_url(), - WorkerParentErrorEventKind::ErrorEvent, + WorkerParentErrorEventKind::Event, ); client.close_ports(); client_id diff --git a/moli-renderer-v8/src/shared_worker_runtime/host_clients.rs b/moli-renderer-v8/src/shared_worker_runtime/host_clients.rs index 82496fc50..aa287634f 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/host_clients.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/host_clients.rs @@ -140,7 +140,7 @@ impl RendererSharedWorkerHost { client_id, "Failed to connect SharedWorker: worker runtime is unavailable.", failure_filename, - WorkerParentErrorEventKind::ErrorEvent, + WorkerParentErrorEventKind::Event, ); false } @@ -156,7 +156,7 @@ impl RendererSharedWorkerHost { client_id, "Failed to connect SharedWorker: worker runtime is unavailable.", "", - WorkerParentErrorEventKind::ErrorEvent, + WorkerParentErrorEventKind::Event, ); failed.push(client_id); } diff --git a/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs b/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs index 65bbe3e09..c9f3c9260 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs @@ -92,7 +92,7 @@ fn finish_loading_with_runtime_service( clients, message, params.key.script_url(), - WorkerParentErrorEventKind::ErrorEvent, + WorkerParentErrorEventKind::Event, ); } } @@ -335,6 +335,7 @@ mod tests { SharedWorkerClientEvent::Error(error) if error.endpoint_disposition() == SharedWorkerClientEndpointDisposition::Retire + && error.event_kind() == WorkerParentErrorEventKind::Event )); assert!( message_port_owner @@ -665,6 +666,7 @@ mod tests { SharedWorkerClientEvent::Error(error) if error.endpoint_disposition() == SharedWorkerClientEndpointDisposition::Retire + && error.event_kind() == WorkerParentErrorEventKind::Event )); assert!( message_port_owner diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 0936c038b..be1dc1f8d 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -2957,12 +2957,16 @@ pub(super) fn dispatch_nested_worker_event( lineno, colno, event_kind, + phase, .. } => { let unhandled = crate::context_bootstrap::dispatch_worker_event(scope, worker, message); + // Bootstrap failures only fire an Event at the child Worker. Only + // uncanceled runtime errors propagate to its owner's global scope. + let propagate = unhandled && *phase == super::handle::WorkerErrorPhase::Runtime; NestedWorkerDispatchResult { dispatched: true, - unhandled_error: unhandled.then(|| NestedWorkerUnhandledError { + unhandled_error: propagate.then(|| NestedWorkerUnhandledError { message: error_message.clone(), filename: filename.clone(), lineno: *lineno, diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index e67955f0a..859b33d1d 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -9335,17 +9335,25 @@ async fn nested_worker_script_load_failure_is_async_error_event() { let mut handle = spawn_worker( r#" let result = "not-run"; + let globalErrors = 0; + onerror = () => { ++globalErrors; return true; }; try { const child = new Worker("missing-child.js"); child.onerror = event => { event.preventDefault(); - postMessage({ + const observation = { constructed: result === "constructed", type: event.type, - messageIsNonEmpty: event.message.length > 0, - filename: event.filename - }); - close(); + intrinsicEvent: Object.getPrototypeOf(event) === Event.prototype, + target: event.target === child, + trusted: event.isTrusted, + flags: [event.bubbles, event.cancelable, event.composed, event.defaultPrevented], + hasErrorDetails: ['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event) + }; + setTimeout(() => { + postMessage({ ...observation, globalErrors }); + close(); + }, 0); }; result = "constructed"; } catch (error) { @@ -9363,9 +9371,7 @@ async fn nested_worker_script_load_failure_is_async_error_event() { .expect("channel closed"); assert_eq!( expect_post_json(msg), - format!( - r#"{{"constructed":true,"type":"error","messageIsNonEmpty":true,"filename":"{base_url}/missing-child.js"}}"# - ) + r#"{"constructed":true,"type":"error","intrinsicEvent":true,"target":true,"trusted":true,"flags":[false,false,false,false],"hasErrorDetails":false,"globalErrors":0}"# ); timeout(TIMEOUT, server) .await @@ -9373,6 +9379,48 @@ async fn nested_worker_script_load_failure_is_async_error_event() { .expect("nested worker script server should finish"); } +#[tokio::test] +async fn nested_worker_parse_errors_use_intrinsic_events_without_global_propagation() { + ensure_v8(); + for child_type in ["classic", "module"] { + let mut handle = spawn_worker( + r#" + const originalEvent = Event; + let globalErrors = 0; + let authorReads = 0; + onerror = () => { ++globalErrors; return true; }; + const child = new Worker("data:text/javascript,function%20(", { type: "CHILD_TYPE" }); + child.onerror = event => { + event.preventDefault(); + const observation = { + type: event.type, + intrinsicEvent: Object.getPrototypeOf(event) === originalEvent.prototype, + target: event.target === child, + trusted: event.isTrusted, + flags: [event.bubbles, event.cancelable, event.composed, event.defaultPrevented], + hasErrorDetails: ['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event) + }; + setTimeout(() => { + postMessage({ ...observation, globalErrors, authorReads }); + close(); + }, 0); + }; + Object.defineProperty(globalThis, "Event", { + configurable: true, + get() { ++authorReads; throw new Error("author Event getter"); } + }); + "# + .replace("CHILD_TYPE", child_type), + "test://nested_worker_parse_error".into(), + ); + assert_eq!( + recv_post_json(&mut handle).await, + r#"{"type":"error","intrinsicEvent":true,"target":true,"trusted":true,"flags":[false,false,false,false],"hasErrorDetails":false,"globalErrors":0,"authorReads":0}"#, + "{child_type} child bootstrap must not invoke author hooks or parent onerror" + ); + } +} + #[tokio::test] async fn worker_onmessage_exception_routes_through_worker_global_onerror() { ensure_v8(); diff --git a/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-basic.html b/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-basic.html index 6a7be1b2b..c62b18f20 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-basic.html +++ b/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-basic.html @@ -3,6 +3,7 @@ + + + + + + +