summaryrefslogtreecommitdiff
path: root/core/bindings.rs
diff options
context:
space:
mode:
authorBen Noordhuis <info@bnoordhuis.nl>2021-11-28 00:46:12 +0100
committerGitHub <noreply@github.com>2021-11-28 00:46:12 +0100
commit2d830c263b0a3519c7fb75199a6ad1b8f10d2b51 (patch)
tree71a045c5b66b13ddaf1f4c43fc6031e0dfa41a8a /core/bindings.rs
parent993a1dd41ae5f96bdb24b09757e24c2ac24126d0 (diff)
feat(core): intercept unhandled promise rejections (#12910)
Provide a programmatic means of intercepting rejected promises without a .catch() handler. Needed for Node compat mode. Also do a first pass at uncaughtException support because they're closely intertwined in Node. It's like that Frank Sinatra song: you can't have one without the other. Stepping stone for #7013.
Diffstat (limited to 'core/bindings.rs')
-rw-r--r--core/bindings.rs188
1 files changed, 151 insertions, 37 deletions
diff --git a/core/bindings.rs b/core/bindings.rs
index b18d24c79..e4c4e6515 100644
--- a/core/bindings.rs
+++ b/core/bindings.rs
@@ -48,6 +48,12 @@ lazy_static::lazy_static! {
function: set_nexttick_callback.map_fn_to()
},
v8::ExternalReference {
+ function: set_promise_reject_callback.map_fn_to()
+ },
+ v8::ExternalReference {
+ function: set_uncaught_exception_callback.map_fn_to()
+ },
+ v8::ExternalReference {
function: run_microtasks.map_fn_to()
},
v8::ExternalReference {
@@ -171,6 +177,18 @@ pub fn initialize_context<'s>(
"setNextTickCallback",
set_nexttick_callback,
);
+ set_func(
+ scope,
+ core_val,
+ "setPromiseRejectCallback",
+ set_promise_reject_callback,
+ );
+ set_func(
+ scope,
+ core_val,
+ "setUncaughtExceptionCallback",
+ set_uncaught_exception_callback,
+ );
set_func(scope, core_val, "runMicrotasks", run_microtasks);
set_func(scope, core_val, "hasTickScheduled", has_tick_scheduled);
set_func(
@@ -320,30 +338,89 @@ pub extern "C" fn host_initialize_import_meta_object_callback(
}
pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
+ use v8::PromiseRejectEvent::*;
+
let scope = &mut unsafe { v8::CallbackScope::new(&message) };
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
- let promise = message.get_promise();
- let promise_global = v8::Global::new(scope, promise);
+ // Node compat: perform synchronous process.emit("unhandledRejection").
+ //
+ // Note the callback follows the (type, promise, reason) signature of Node's
+ // internal promiseRejectHandler from lib/internal/process/promises.js, not
+ // the (promise, reason) signature of the "unhandledRejection" event listener.
+ //
+ // Short-circuits Deno's regular unhandled rejection logic because that's
+ // a) asynchronous, and b) always terminates.
+ if let Some(js_promise_reject_cb) = state.js_promise_reject_cb.clone() {
+ let js_uncaught_exception_cb = state.js_uncaught_exception_cb.clone();
+ drop(state); // Drop borrow, callbacks can call back into runtime.
+
+ let tc_scope = &mut v8::TryCatch::new(scope);
+ let undefined: v8::Local<v8::Value> = v8::undefined(tc_scope).into();
+ let type_ = v8::Integer::new(tc_scope, message.get_event() as i32);
+ let promise = message.get_promise();
+
+ let reason = match message.get_event() {
+ PromiseRejectWithNoHandler
+ | PromiseRejectAfterResolved
+ | PromiseResolveAfterResolved => message.get_value().unwrap_or(undefined),
+ PromiseHandlerAddedAfterReject => undefined,
+ };
- match message.get_event() {
- v8::PromiseRejectEvent::PromiseRejectWithNoHandler => {
- let error = message.get_value().unwrap();
- let error_global = v8::Global::new(scope, error);
- state
- .pending_promise_exceptions
- .insert(promise_global, error_global);
+ let args = &[type_.into(), promise.into(), reason];
+ js_promise_reject_cb
+ .open(tc_scope)
+ .call(tc_scope, undefined, args);
+
+ if let Some(exception) = tc_scope.exception() {
+ if let Some(js_uncaught_exception_cb) = js_uncaught_exception_cb {
+ tc_scope.reset(); // Cancel pending exception.
+ js_uncaught_exception_cb.open(tc_scope).call(
+ tc_scope,
+ undefined,
+ &[exception],
+ );
+ }
}
- v8::PromiseRejectEvent::PromiseHandlerAddedAfterReject => {
- state.pending_promise_exceptions.remove(&promise_global);
+
+ if tc_scope.has_caught() {
+ // If we get here, an exception was thrown by the unhandledRejection
+ // handler and there is ether no uncaughtException handler or the
+ // handler threw an exception of its own.
+ //
+ // TODO(bnoordhuis) Node terminates the process or worker thread
+ // but we don't really have that option. The exception won't bubble
+ // up either because V8 cancels it when this function returns.
+ let exception = tc_scope
+ .stack_trace()
+ .or_else(|| tc_scope.exception())
+ .map(|value| value.to_rust_string_lossy(tc_scope))
+ .unwrap_or_else(|| "no exception".into());
+ eprintln!("Unhandled exception: {}", exception);
}
- v8::PromiseRejectEvent::PromiseRejectAfterResolved => {}
- v8::PromiseRejectEvent::PromiseResolveAfterResolved => {
- // Should not warn. See #1272
+ } else {
+ let promise = message.get_promise();
+ let promise_global = v8::Global::new(scope, promise);
+
+ match message.get_event() {
+ PromiseRejectWithNoHandler => {
+ let error = message.get_value().unwrap();
+ let error_global = v8::Global::new(scope, error);
+ state
+ .pending_promise_exceptions
+ .insert(promise_global, error_global);
+ }
+ PromiseHandlerAddedAfterReject => {
+ state.pending_promise_exceptions.remove(&promise_global);
+ }
+ PromiseRejectAfterResolved => {}
+ PromiseResolveAfterResolved => {
+ // Should not warn. See #1272
+ }
}
- };
+ }
}
fn opcall_sync<'s>(
@@ -545,15 +622,12 @@ fn set_nexttick_callback(
args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue,
) {
- let state_rc = JsRuntime::state(scope);
- let mut state = state_rc.borrow_mut();
-
- let cb = match v8::Local::<v8::Function>::try_from(args.get(0)) {
- Ok(cb) => cb,
- Err(err) => return throw_type_error(scope, err.to_string()),
- };
-
- state.js_nexttick_cbs.push(v8::Global::new(scope, cb));
+ if let Ok(cb) = arg0_to_cb(scope, args) {
+ JsRuntime::state(scope)
+ .borrow_mut()
+ .js_nexttick_cbs
+ .push(cb);
+ }
}
fn set_macrotask_callback(
@@ -561,15 +635,55 @@ fn set_macrotask_callback(
args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue,
) {
- let state_rc = JsRuntime::state(scope);
- let mut state = state_rc.borrow_mut();
+ if let Ok(cb) = arg0_to_cb(scope, args) {
+ JsRuntime::state(scope)
+ .borrow_mut()
+ .js_macrotask_cbs
+ .push(cb);
+ }
+}
- let cb = match v8::Local::<v8::Function>::try_from(args.get(0)) {
- Ok(cb) => cb,
- Err(err) => return throw_type_error(scope, err.to_string()),
- };
+fn set_promise_reject_callback(
+ scope: &mut v8::HandleScope,
+ args: v8::FunctionCallbackArguments,
+ mut rv: v8::ReturnValue,
+) {
+ if let Ok(new) = arg0_to_cb(scope, args) {
+ if let Some(old) = JsRuntime::state(scope)
+ .borrow_mut()
+ .js_promise_reject_cb
+ .replace(new)
+ {
+ let old = v8::Local::new(scope, old);
+ rv.set(old.into());
+ }
+ }
+}
- state.js_macrotask_cbs.push(v8::Global::new(scope, cb));
+fn set_uncaught_exception_callback(
+ scope: &mut v8::HandleScope,
+ args: v8::FunctionCallbackArguments,
+ mut rv: v8::ReturnValue,
+) {
+ if let Ok(new) = arg0_to_cb(scope, args) {
+ if let Some(old) = JsRuntime::state(scope)
+ .borrow_mut()
+ .js_uncaught_exception_cb
+ .replace(new)
+ {
+ let old = v8::Local::new(scope, old);
+ rv.set(old.into());
+ }
+ }
+}
+
+fn arg0_to_cb(
+ scope: &mut v8::HandleScope,
+ args: v8::FunctionCallbackArguments,
+) -> Result<v8::Global<v8::Function>, ()> {
+ v8::Local::<v8::Function>::try_from(args.get(0))
+ .map(|cb| v8::Global::new(scope, cb))
+ .map_err(|err| throw_type_error(scope, err.to_string()))
}
fn eval_context(
@@ -707,19 +821,19 @@ fn set_wasm_streaming_callback(
) {
use crate::ops_builtin::WasmStreamingResource;
- let state_rc = JsRuntime::state(scope);
- let mut state = state_rc.borrow_mut();
-
- let cb = match v8::Local::<v8::Function>::try_from(args.get(0)) {
+ let cb = match arg0_to_cb(scope, args) {
Ok(cb) => cb,
- Err(err) => return throw_type_error(scope, err.to_string()),
+ Err(()) => return,
};
+ let state_rc = JsRuntime::state(scope);
+ let mut state = state_rc.borrow_mut();
+
// The callback to pass to the v8 API has to be a unit type, so it can't
// borrow or move any local variables. Therefore, we're storing the JS
// callback in a JsRuntimeState slot.
if let slot @ None = &mut state.js_wasm_streaming_cb {
- slot.replace(v8::Global::new(scope, cb));
+ slot.replace(cb);
} else {
return throw_type_error(
scope,