Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tm-android): Reject Promise if Turbo Module method throws an Error #37484

Closed
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
097525d
Add iOS reject promise if turbo module method throws exception
krystofwoldrich Apr 27, 2023
4fc71fd
Merge remote-tracking branch 'origin/main' into kw-mixed-stack-traces…
krystofwoldrich May 3, 2023
108e205
Merge remote-tracking branch 'origin/main' into kw-mixed-stack-traces…
krystofwoldrich May 14, 2023
0ca58bb
Merge branch 'main' into kw-mixed-stack-traces-promise
krystofwoldrich May 15, 2023
5892e50
Add Android reject async func on error
krystofwoldrich May 15, 2023
97606c9
add(ios): js stack to rejection caused by native exception or native …
krystofwoldrich May 16, 2023
2eb0a46
Fix android promise reject and add JS stack trace
krystofwoldrich May 17, 2023
a836691
Refactor java callback
krystofwoldrich May 18, 2023
be2ea63
Fix typos
krystofwoldrich May 18, 2023
ccf5460
iOS jsStack do not copy
krystofwoldrich May 18, 2023
85f48d7
Update lint
krystofwoldrich May 18, 2023
7205f1d
More lint
krystofwoldrich May 18, 2023
d92339c
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Jun 6, 2023
99cb0b9
Add RCT_TRACE_PROMISE_REJECTION_ENABLED flag for iOS
krystofwoldrich Jun 6, 2023
732dba9
Rename the iOS flag
krystofwoldrich Jun 6, 2023
239667d
Add android flag and fix internal reject invocation on Hermes
krystofwoldrich Jun 14, 2023
882c81b
move RCTInternalPromiseRejectBlock to rct turbo module
krystofwoldrich Jun 14, 2023
9529dd9
Add js func returning void rethrow explanation
krystofwoldrich Jun 14, 2023
40163ff
Rename RCTInternalPromiseRejectBlock to RCTNSExceptionHandler
krystofwoldrich Jun 15, 2023
49b861d
Add CallbackWrapperExecutor type
krystofwoldrich Jun 15, 2023
4d40ceb
Handle all throws from Obj-Cpp
krystofwoldrich Jun 15, 2023
86bf079
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Aug 9, 2023
b21990c
Merge branch 'main' into kw-mixed-stack-traces-promise
krystofwoldrich Aug 10, 2023
25b32b7
Enable trace turbo module rejection in debug iOS builds
krystofwoldrich Aug 11, 2023
be8e027
Make jsInvocationStack optional
krystofwoldrich Aug 11, 2023
377c328
Add jsInvocationStack comment
krystofwoldrich Aug 11, 2023
6e4b652
use ref to js stack in createRejectJSErrorValue
krystofwoldrich Aug 11, 2023
abf7e8a
Use only one rejectJSIFn in java
krystofwoldrich Aug 11, 2023
e1634a6
Rename feature flag
krystofwoldrich Aug 11, 2023
65982e2
fix flag func overload
krystofwoldrich Aug 11, 2023
ba06a6e
Update feature flag
krystofwoldrich Aug 11, 2023
c6dd575
Merge commit '60f5a80c1d5900e5213aabd3d3a762174996cfd1' into kw-mixed…
krystofwoldrich Oct 9, 2023
2a3f8ea
enable tracing turbo modules promises rejections in debug
krystofwoldrich Oct 9, 2023
bd9b7f1
unify naming
krystofwoldrich Oct 9, 2023
3f81d5a
Use jsi types for callback executor
krystofwoldrich Oct 10, 2023
1936174
access cause safe
krystofwoldrich Oct 10, 2023
975a511
keep js invocation stack as property
krystofwoldrich Oct 10, 2023
bf584da
Hold reference to stack to avoid parsing string
krystofwoldrich Oct 10, 2023
3937f61
Add explanation comment
krystofwoldrich Oct 10, 2023
c735ba9
Add test errors logs for easier verification
krystofwoldrich Oct 10, 2023
7e84c08
Add rejectTurboModulePromiseOnNativeError flag
krystofwoldrich Oct 10, 2023
f0f515b
Drop iOS changes
krystofwoldrich Oct 10, 2023
7359585
One more header
krystofwoldrich Oct 10, 2023
0025fbd
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Oct 18, 2023
422f10f
review fixes, remove build config, unwrap callback executor, safe han…
krystofwoldrich Oct 19, 2023
b0a1ed5
Remove unused var
krystofwoldrich Oct 19, 2023
26683c5
Merge remote-tracking branch 'upstream/main' into kw-mixed-stack-trac…
krystofwoldrich Oct 23, 2023
fdbde12
Use async callback WIP -> returns undefined
krystofwoldrich Oct 24, 2023
d545623
Clean up, fix reject index, revert stack to string add comments
krystofwoldrich Oct 24, 2023
d77f909
trace rejection based on debug config flag
krystofwoldrich Oct 24, 2023
bc505bf
Update based on comments
krystofwoldrich Oct 24, 2023
e99fb6a
Use global ref for throwable
krystofwoldrich Oct 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
RCTEnableTurboModule(turboModuleEnabled);
#endif

#ifndef RCT_TRACE_PROMISE_REJECTION_ENABLED
#if DEBUG
#define RCT_TRACE_PROMISE_REJECTION_ENABLED 1
#endif
#endif

#if RCT_TRACE_PROMISE_REJECTION_ENABLED
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we enable this by default in development/debug builds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that definitely make sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For iOS I solve it by compiler directives if a user doesn't set the flag and the build is a debug build then enabled.

But for Java, I'm not sure if we can do the same, as using the ReactFeatureFlags we can't tell if a user flipped the flag or not, did I miss something or how would you solve it?

Copy link
Member

@javache javache Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Java, you could default init the ReactFeatureFlags with ReactBuildConfig.DEBUG, but we probably should move this out of ReactFeatureFlags as well, since that's only intended for experimental features.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added ReactBuildConfig.DEBUG for now

RCTEnableTraceTurboModulePromiseRejections(YES);
#endif

#if DEBUG
// Disable idle timer in dev builds to avoid putting application in background and complicating
// Metro reconnection logic. Users only need this when running the application using our CLI tooling.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@ else
source[:tag] = "v#{version}"
end

trace_promise_rejections_enabled = ENV["RCT_TRACE_PROMISE_REJECTION_ENABLED"] == "1"
trace_promise_rejections_flag = (trace_promise_rejections_enabled ? " -DRCT_TRACE_PROMISE_REJECTION_ENABLED" : "")

folly_flags = ' -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'
folly_compiler_flags = folly_flags + ' ' + '-Wno-comma -Wno-shorten-64-to-32'

is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1"

use_hermes = ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == '1'
use_frameworks = ENV['USE_FRAMEWORKS'] != nil

new_arch_enabled_flag = (is_new_arch_enabled ? " -DRCT_NEW_ARCH_ENABLED" : "")
is_fabric_enabled = is_new_arch_enabled || ENV["RCT_FABRIC_ENABLED"]
fabric_flag = (is_fabric_enabled ? " -DRN_FABRIC_ENABLED" : "")
hermes_flag = (use_hermes ? " -DUSE_HERMES" : "")
other_cflags = "$(inherited)" + folly_flags + new_arch_enabled_flag + fabric_flag + hermes_flag
other_cflags = "$(inherited)" + folly_flags + new_arch_enabled_flag + fabric_flag + hermes_flag + trace_promise_rejections_flag

header_search_paths = [
"$(PODS_TARGET_SRCROOT)/../../ReactCommon",
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native/React/Base/RCTBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ RCT_EXTERN void RCTSetTurboModuleInteropBridgeProxyLogLevel(RCTBridgeProxyLoggin
RCT_EXTERN BOOL RCTTurboModuleInteropForAllTurboModulesEnabled(void);
RCT_EXTERN void RCTEnableTurboModuleInteropForAllTurboModules(BOOL enabled);

// Trace Rejected Promises of Turbo Modules (store callers' js stack)
RCT_EXTERN BOOL RCTTraceTurboModulePromiseRejections(void);
RCT_EXTERN void RCTEnableTraceTurboModulePromiseRejections(BOOL enabled);

typedef enum {
kRCTGlobalScope,
kRCTGlobalScopeUsingRetainJSCallback,
Expand Down
34 changes: 34 additions & 0 deletions packages/react-native/React/Base/RCTBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ void RCTEnableTurboModule(BOOL enabled)
turboModuleEnabled = enabled;
}

static BOOL turboModuleEagerInitEnabled = NO;
BOOL RCTTurboModuleEagerInitEnabled(void)
{
return turboModuleEagerInitEnabled;
}

static BOOL traceTurboModulePromiseRejections = NO;
BOOL RCTTraceTurboModulePromiseRejections(void)
{
return traceTurboModulePromiseRejections;
}

void RCTEnableTraceTurboModulePromiseRejections(BOOL enabled)
{
traceTurboModulePromiseRejections = enabled;
}

void RCTEnableTurboModuleEagerInit(BOOL enabled)
{
turboModuleEagerInitEnabled = enabled;
}

// Turn off TurboModule delegate locking
static BOOL turboModuleManagerDelegateLockingDisabled = YES;
BOOL RCTTurboModuleManagerDelegateLockingDisabled(void)
{
return turboModuleManagerDelegateLockingDisabled;
}

void RCTDisableTurboModuleManagerDelegateLocking(BOOL disabled)
{
turboModuleManagerDelegateLockingDisabled = disabled;
}

krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
static BOOL turboModuleInteropEnabled = NO;
BOOL RCTTurboModuleInteropEnabled(void)
{
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Base/RCTUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
NSArray<NSString *> *stackTrace = [NSThread callStackSymbols];
NSMutableDictionary *userInfo;
NSMutableDictionary<NSString *, id> *errorInfo = [NSMutableDictionary dictionaryWithObject:stackTrace
forKey:@"nativeStackIOS"];
forKey:@"stackSymbols"];

if (error) {
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,10 @@ public class ReactFeatureFlags {

/** Clean yoga node when <Text /> does not change. */
public static boolean enableCleanParagraphYogaNode = false;

/**
* Enables storing js caller stack when creating promise in native module.
* This is useful in case of Promise rejection and tracing the cause.
*/
public static boolean traceTurboModulePromiseRejections = false;
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,32 @@ JavaTurboModule::~JavaTurboModule() {

namespace {

constexpr static auto kReactFeatureFlagsJavaDescriptor = "com/facebook/react/config/ReactFeatureFlags";

static bool getFeatureFlagBoolValue(const char *name) {
static const auto reactFeatureFlagsClass = facebook::jni::findClassStatic(kReactFeatureFlagsJavaDescriptor);
const auto field = reactFeatureFlagsClass->getStaticField<jboolean>(name);
return reactFeatureFlagsClass->getStaticFieldValue(field);
}

static bool isSavePromiseJSInvocationStackEnabled() {
static bool savePromiseJSInvocationStack = getFeatureFlagBoolValue("traceTurboModulePromiseRejections");
return savePromiseJSInvocationStack;
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

struct JNIArgs {
JNIArgs(size_t count) : args_(count) {}
std::vector<jvalue> args_;
std::vector<jobject> globalRefs_;
};

using CallbackWrapperExecutor = std::function<void(const std::shared_ptr<CallbackWrapper>&, const std::vector<jsi::Value>&)>;
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

jni::local_ref<JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
jsi::Function &&function,
jsi::Runtime &rt,
const std::shared_ptr<CallInvoker> &jsInvoker) {
const std::shared_ptr<CallInvoker> &jsInvoker,
CallbackWrapperExecutor&& callbackWrapperExecutor) {
auto weakWrapper =
CallbackWrapper::createWeak(std::move(function), rt, jsInvoker);

Expand All @@ -82,7 +98,8 @@ jni::local_ref<JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
return JCxxCallbackImpl::newObjectCxxArgs(
[weakWrapper = std::move(weakWrapper),
callbackWrapperOwner = std::move(callbackWrapperOwner),
wrapperWasCalled = false](folly::dynamic responses) mutable {
wrapperWasCalled = false,
callbackWrapperExecutor = std::move(callbackWrapperExecutor)](folly::dynamic responses) mutable {
if (wrapperWasCalled) {
LOG(FATAL) << "callback arg cannot be called more than once";
}
Expand All @@ -95,7 +112,8 @@ jni::local_ref<JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
strongWrapper->jsInvoker().invokeAsync(
[weakWrapper = std::move(weakWrapper),
callbackWrapperOwner = std::move(callbackWrapperOwner),
responses = std::move(responses)]() {
responses = std::move(responses),
callbackWrapperExecutor = std::move(callbackWrapperExecutor)]() {
auto strongWrapper2 = weakWrapper.lock();
if (!strongWrapper2) {
return;
Expand All @@ -108,16 +126,88 @@ jni::local_ref<JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
jsi::valueFromDynamic(strongWrapper2->runtime(), val));
}

strongWrapper2->callback().call(
strongWrapper2->runtime(),
(const jsi::Value *)args.data(),
args.size());
callbackWrapperExecutor(strongWrapper2, args);
});

wrapperWasCalled = true;
});
}

jni::local_ref<JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
jsi::Function &&function,
jsi::Runtime &rt,
const std::shared_ptr<CallInvoker> &jsInvoker) {
auto executor = [](
const std::shared_ptr<CallbackWrapper>& wrapper,
const std::vector<jsi::Value>& args) {
wrapper->callback().call(
wrapper->runtime(),
(const jsi::Value *)args.data(),
args.size());
};
return createJavaCallbackFromJSIFunction(std::move(function), rt, jsInvoker, std::move(executor));
}

jsi::Value createJSRuntimeError(jsi::Runtime &runtime, const std::string &message);

jni::local_ref<JCxxCallbackImpl::JavaPart> createPromiseRejectJavaCallbackFromJSIFunction(
const std::weak_ptr<CallbackWrapper> &function,
jsi::Runtime &rt,
const std::shared_ptr<CallInvoker> &jsInvoker,
std::optional<std::string> jsInvocationStack) {
auto executor = [jsInvocationStack = std::move(jsInvocationStack)](
const std::shared_ptr<CallbackWrapper>& reject,
const std::vector<jsi::Value>& args) {
std::string message;
jsi::Runtime &rt2 = reject->runtime();
const jsi::Value* cause = args.data();
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
if (cause->isObject() && cause->asObject(rt2).getProperty(rt2, "message").isString()) {
message = cause->asObject(rt2).getProperty(rt2, "message").asString(rt2).utf8(rt2);
} else {
message = "<unknown>";
}

jsi::Value error = createJSRuntimeError(rt2, "Exception in HostFunction: " + message);
error.asObject(rt2).setProperty(rt2, "cause", *cause);
error.asObject(rt2).setProperty(rt2, "stack", *jsInvocationStack);
reject->callback().call(rt2, error);
};
return JCxxCallbackImpl::newObjectCxxArgs(
[function,
wrapperWasCalled = false,
executor = std::move(executor)](folly::dynamic responses) mutable {
if (wrapperWasCalled) {
LOG(FATAL) << "callback arg cannot be called more than once";
}

auto strongWrapper = function.lock();
if (!strongWrapper) {
return;
}

strongWrapper->jsInvoker().invokeAsync(
[function,
responses = std::move(responses),
executor = std::move(executor)]() {
auto strongWrapper2 = function.lock();
if (!strongWrapper2) {
return;
}

std::vector<jsi::Value> args;
args.reserve(responses.size());
for (const auto &val : responses) {
args.emplace_back(
jsi::valueFromDynamic(strongWrapper2->runtime(), val));
}

executor(strongWrapper2, args);
});

wrapperWasCalled = true;
});
}

// This is used for generating short exception strings.
std::string stringifyJSIValue(const jsi::Value &v, jsi::Runtime *rt = nullptr) {
if (v.isUndefined()) {
Expand Down Expand Up @@ -442,6 +532,35 @@ jsi::JSError convertThrowableToJSError(
return {runtime, std::move(error)};
}

void rejectWithException(
std::weak_ptr<CallbackWrapper> &rejectWeak,
std::exception_ptr &exception,
std::optional<std::string> jsInvocationStack) {
auto reject = rejectWeak.lock();
if (reject) {
reject->jsInvoker().invokeAsync([
rejectWeak = std::move(rejectWeak),
jsInvocationStack = std::move(jsInvocationStack),
exception
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
]() {
auto reject2 = rejectWeak.lock();
if (reject2) {
jsi::Runtime &runtime = reject2->runtime();
auto throwable = jni::getJavaExceptionForCppException(exception);
auto jsError = convertThrowableToJSError(runtime, throwable);
if (jsInvocationStack.has_value()) {
jsError.value().asObject(runtime).setProperty(runtime, "stack", *jsInvocationStack);
}
reject2->callback().call(
runtime,
jsError.value());
}
});
} else {
throw;
}
}

} // namespace

jsi::Value JavaTurboModule::invokeJavaMethod(
Expand Down Expand Up @@ -801,18 +920,30 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
throw std::invalid_argument("Promise fn arg count must be 2");
}

// JS Stack at the time when the promise is created.
std::optional<std::string> jsInvocationStack;
if (isSavePromiseJSInvocationStackEnabled()) {
jsInvocationStack = createJSRuntimeError(runtime, "")
.asObject(runtime)
.getProperty(runtime, "stack")
.asString(runtime)
.utf8(runtime);
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
}

jsi::Function resolveJSIFn =
promiseConstructorArgs[0].getObject(runtime).getFunction(
runtime);
jsi::Function rejectJSIFn =
promiseConstructorArgs[1].getObject(runtime).getFunction(
runtime);
auto rejectJSIFunctionWeakWrapper =
CallbackWrapper::createWeak(std::move(rejectJSIFn), runtime, jsInvoker_);

auto resolve = createJavaCallbackFromJSIFunction(
std::move(resolveJSIFn), runtime, jsInvoker_)
.release();
auto reject = createJavaCallbackFromJSIFunction(
std::move(rejectJSIFn), runtime, jsInvoker_)
std::move(resolveJSIFn), runtime, jsInvoker_)
.release();
auto reject = createPromiseRejectJavaCallbackFromJSIFunction(
rejectJSIFunctionWeakWrapper, runtime, jsInvoker_, jsInvocationStack)
.release();

jclass jPromiseImpl =
Expand Down Expand Up @@ -840,6 +971,8 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
nativeMethodCallInvoker_->invokeAsync(
methodName,
[jargs,
rejectJSIFunctionWeakWrapper,
jsInvocationStack = std::move(jsInvocationStack),
globalRefs,
methodID,
instance_ = jni::make_weak(instance_),
Expand Down Expand Up @@ -868,7 +1001,8 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
} catch (...) {
TMPL::asyncMethodCallExecutionFail(
moduleName, methodName, id);
throw;
auto exception = std::current_exception();
rejectWithException(rejectJSIFunctionWeakWrapper, exception, jsInvocationStack);
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
}

for (auto globalRef : globalRefs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTTurboModule)]))
#define RCT_IS_TURBO_MODULE_INSTANCE(module) RCT_IS_TURBO_MODULE_CLASS([(module) class])

/**
* Block used to reject the JS promise waiting for a result
* in cases when the native method throws an exception.
*/
typedef void (^RCTNSDictionaryPromiseRejectBlock)(NSDictionary *exception);

namespace facebook::react {

class CallbackWrapper;
Expand Down Expand Up @@ -137,9 +143,10 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule {
bool isSync,
const char *methodName,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation);
NSMutableArray *retainedObjectsForInvocation,
RCTNSDictionaryPromiseRejectBlock optionalInternalRejectBlock);

using PromiseInvocationBlock = void (^)(RCTPromiseResolveBlock resolveWrapper, RCTPromiseRejectBlock rejectWrapper);
using PromiseInvocationBlock = void (^)(RCTPromiseResolveBlock resolveWrapper, RCTPromiseRejectBlock rejectWrapper, RCTNSDictionaryPromiseRejectBlock internalRejectWrapper);
jsi::Value createPromise(jsi::Runtime &runtime, std::string methodName, PromiseInvocationBlock invoke);
};

Expand Down
Loading