fix(events): suppress reentrant rejectionhandled events

Extract an independently mergeable topic from wpt-misc-fix at
a70a96f9d1.

Source commits:
- 3fd12f80b7

Validation:
- cargo fmt --all
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo nextest run --no-fail-fast
This commit is contained in:
ldm0
2026-09-10 05:46:08 +08:00
committed by Donough Liu
parent 23a488be68
commit 9c338eedf5
4 changed files with 87 additions and 12 deletions
@@ -121,7 +121,6 @@ pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>)
.reason
.as_ref()
.map(|reason| v8::Local::new(scope, reason));
remember_reported_promise_rejection(&state.reported, rejection.clone());
let outcome = dispatch_window_promise_rejection_event(
scope,
host_ptr,
@@ -130,6 +129,13 @@ pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>)
promise,
reason,
);
// A handler added by the `unhandledrejection` listener must not
// synchronously turn this notification into `rejectionhandled`.
// Only promises that remain unhandled after dispatch belong in
// the outstanding reported-rejection set.
if !promise.has_handler() {
remember_reported_promise_rejection(&state.reported, rejection.clone());
}
if matches!(outcome, Ok(true)) {
log_unhandled_promise_rejection(scope, reason);
}
@@ -25,6 +25,40 @@ Promise.reject("main-owned");
);
}
#[test]
fn handler_added_during_unhandled_rejection_does_not_dispatch_rejectionhandled() {
let mut vm = new_storage_test_vm("https://handled-during-notification.test/");
vm.eval(
r#"
globalThis.__handledDuringNotificationEvents = [];
const reason = new Error("handled-during-notification");
const rejected = Promise.reject(reason);
onunhandledrejection = event => {
__handledDuringNotificationEvents.push(event.type);
event.preventDefault();
rejected.catch(value => {
__handledDuringNotificationEvents.push(value === reason ? "handler" : "wrong-reason");
});
};
onrejectionhandled = event => {
__handledDuringNotificationEvents.push(event.type);
};
"#,
)
.expect("rejection handled during notification setup should evaluate");
vm.eval("0")
.expect("unhandled rejection notification checkpoint should evaluate");
vm.eval("0")
.expect("rejection handler reaction checkpoint should evaluate");
assert_eq!(
vm.eval("JSON.stringify(__handledDuringNotificationEvents)")
.expect("rejection handled during notification result should evaluate"),
r#"["unhandledrejection","handler"]"#
);
}
#[test]
fn universal_isolated_world_rejection_uses_its_registry_backed_realm() {
let mut vm = new_storage_test_vm("https://isolated-promise-rejection.test/");
+14 -11
View File
@@ -500,10 +500,6 @@ fn flush_pending_worker_promise_rejections(scope: &mut v8::PinScope<'_, '_>) {
.reason
.as_ref()
.map(|reason| v8::Local::new(scope, reason));
remember_reported_worker_promise_rejection(
&reported_unhandled_rejections,
rejection.clone(),
);
let allows_default = dispatch_worker_promise_rejection_event(
scope,
"unhandledrejection",
@@ -512,6 +508,14 @@ fn flush_pending_worker_promise_rejections(scope: &mut v8::PinScope<'_, '_>) {
&parent_tx,
&script_url,
);
// Do not expose a `rejectionhandled` transition when the handler was
// attached by the `unhandledrejection` listener itself.
if !promise.has_handler() {
remember_reported_worker_promise_rejection(
&reported_unhandled_rejections,
rejection.clone(),
);
}
if allows_default {
log_unhandled_promise_rejection(scope, reason);
}
@@ -582,14 +586,13 @@ unsafe extern "C" fn worker_promise_reject_callback(message: v8::PromiseRejectMe
})
.map(|index| reported.swap_remove(index))
};
let reason = reported
let Some(rejection) = reported else {
return;
};
let reason = rejection
.reason
.as_ref()
.and_then(|rejection| {
rejection
.reason
.as_ref()
.map(|reason| v8::Local::new(scope, reason))
})
.map(|reason| v8::Local::new(scope, reason))
.or_else(|| message.get_value());
let _ = dispatch_worker_promise_rejection_event(
scope,
@@ -8661,6 +8661,38 @@ async fn worker_global_rejectionhandled_event_dispatches_for_late_handler() {
);
}
#[tokio::test]
async fn worker_handler_added_during_unhandled_rejection_suppresses_rejectionhandled() {
ensure_v8();
let mut handle = spawn_worker(
r#"
const events = [];
const rejected = Promise.reject("handled-during-worker-notification");
onunhandledrejection = event => {
event.preventDefault();
events.push(event.type);
rejected.catch(reason => {
events.push(reason === "handled-during-worker-notification"
? "handler"
: "wrong-reason");
});
setTimeout(() => {
postMessage(events);
close();
}, 0);
};
onrejectionhandled = event => events.push(event.type);
"#
.into(),
"test://worker_handled_during_unhandledrejection".into(),
);
assert_eq!(
recv_post_json(&mut handle).await,
r#"["unhandledrejection","handler"]"#
);
}
#[tokio::test]
async fn worker_unhandledrejection_notification_allows_one_message_port_turn() {
ensure_v8();