Skip to content

Commit

Permalink
objectwrap: gracefully handle constructor exceptions
Browse files Browse the repository at this point in the history
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Fixes: nodejs#599
  • Loading branch information
Gabriel Schulhof committed Nov 15, 2019
1 parent df75e08 commit f9a8178
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 0 deletions.
23 changes: 23 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -3370,9 +3370,32 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
}

T* instance;
auto removeFailedWrap = [](const CallbackInfo& info) {
napi_status status = napi_remove_wrap(info.Env(), info.This(), nullptr);
NAPI_FATAL_IF_FAILED(status,
"ObjectWrap<T>::ConstructorCallbackWrapper",
"Failed to remove wrap from failed ObjectWrap instance construction");
};
napi_value wrapper = details::WrapCallback([&] {
CallbackInfo callbackInfo(env, info);
#ifdef NAPI_CPP_EXCEPTIONS
try {
instance = new T(callbackInfo);
} catch (const Error& e) {
removeFailedWrap(callbackInfo);
// re-throw the error after removing the failed wrap.
throw e;
}
#else
instance = new T(callbackInfo);
if (callbackInfo.Env().IsExceptionPending()) {
// We need to clear the exception so that `napi_remove_wrap()` might work.
Error e = callbackInfo.Env().GetAndClearPendingException();
removeFailedWrap(callbackInfo);
e.ThrowAsJavaScriptException();
delete instance;
}
# endif // NAPI_CPP_EXCEPTIONS
return callbackInfo.This();
});

Expand Down
3 changes: 3 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Object InitThreadSafeFunction(Env env);
#endif
Object InitTypedArray(Env env);
Object InitObjectWrap(Env env);
Object InitObjectWrapConstructorException(Env env);
Object InitObjectReference(Env env);
Object InitVersionManagement(Env env);
Object InitThunkingManual(Env env);
Expand Down Expand Up @@ -100,6 +101,8 @@ Object Init(Env env, Object exports) {
#endif
exports.Set("typedarray", InitTypedArray(env));
exports.Set("objectwrap", InitObjectWrap(env));
exports.Set("objectwrapConstructorException",
InitObjectWrapConstructorException(env));
exports.Set("objectreference", InitObjectReference(env));
exports.Set("version_management", InitVersionManagement(env));
exports.Set("thunking_manual", InitThunkingManual(env));
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
'threadsafe_function/threadsafe_function.cc',
'typedarray.cc',
'objectwrap.cc',
'objectwrap_constructor_exception.cc',
'objectreference.cc',
'version_management.cc',
'thunking_manual.cc',
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ let testModules = [
'typedarray',
'typedarray-bigint',
'objectwrap',
'objectwrap_constructor_exception',
'objectreference',
'version_management'
];
Expand Down
26 changes: 26 additions & 0 deletions test/objectwrap_constructor_exception.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#include <napi.h>

class ConstructorExceptionTest :
public Napi::ObjectWrap<ConstructorExceptionTest> {
public:
ConstructorExceptionTest(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<ConstructorExceptionTest>(info) {
Napi::Error error = Napi::Error::New(info.Env(), "an exception");
#ifdef NAPI_DISABLE_CPP_EXCEPTIONS
error.ThrowAsJavaScriptException();
#else
throw error;
#endif // NAPI_DISABLE_CPP_EXCEPTIONS
}

static void Initialize(Napi::Env env, Napi::Object exports) {
const char* name = "ConstructorExceptionTest";
exports.Set(name, DefineClass(env, name, {}));
}
};

Napi::Object InitObjectWrapConstructorException(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
ConstructorExceptionTest::Initialize(env, exports);
return exports;
}
19 changes: 19 additions & 0 deletions test/objectwrap_constructor_exception.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';
const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');

const test = (binding) => {
const { ConstructorExceptionTest } = binding.objectwrapConstructorException;
let gotException = false;
try {
new ConstructorExceptionTest();
} catch (anException) {
gotException = true;
}
global.gc();

assert.strictEqual(gotException, true);
}

test(require(`./build/${buildType}/binding.node`));
test(require(`./build/${buildType}/binding_noexcept.node`));

0 comments on commit f9a8178

Please sign in to comment.