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 12 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
2 changes: 2 additions & 0 deletions packages/react-native/React/Base/RCTBridgeModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ typedef void (^RCTPromiseResolveBlock)(id result);
*/
typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error);

typedef void (^RCTInternalPromiseRejectBlock)(NSException *exception);
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

RCT_EXTERN_C_BEGIN

typedef struct RCTMethodInfo {
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 @@ -64,7 +64,8 @@ struct JNIArgs {
jni::local_ref<JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
jsi::Function &&function,
jsi::Runtime &rt,
const std::shared_ptr<CallInvoker> &jsInvoker) {
const std::shared_ptr<CallInvoker> &jsInvoker,
std::function<void(const std::shared_ptr<CallbackWrapper>&, const std::vector<jsi::Value>&)>&& executor) {
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
auto weakWrapper =
CallbackWrapper::createWeak(std::move(function), rt, jsInvoker);

Expand All @@ -80,7 +81,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,
executor = std::move(executor)](folly::dynamic responses) mutable {
if (wrapperWasCalled) {
throw std::runtime_error(
"Callback arg cannot be called more than once");
Expand All @@ -94,7 +96,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),
executor = std::move(executor)]() {
auto strongWrapper2 = weakWrapper.lock();
if (!strongWrapper2) {
return;
Expand All @@ -107,16 +110,55 @@ jni::local_ref<JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
jsi::valueFromDynamic(strongWrapper2->runtime(), val));
}

strongWrapper2->callback().call(
strongWrapper2->runtime(),
(const jsi::Value *)args.data(),
args.size());
executor(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(
jsi::Function &&function,
jsi::Runtime &rt,
const std::shared_ptr<CallInvoker> &jsInvoker,
std::shared_ptr<std::string> jsStack) {
auto executor = [jsStack = std::move(jsStack)](
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", *jsStack);
reject->callback().call(rt2, error);
};
return createJavaCallbackFromJSIFunction(std::move(function), rt, jsInvoker, std::move(executor));
}

// 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 @@ -447,6 +489,19 @@ jsi::JSError convertThrowableToJSError(
return {runtime, std::move(error)};
}

void rejectWithException(
std::shared_ptr<CallbackWrapper> &reject,
std::exception_ptr &exception,
std::string &jsStack) {
jsi::Runtime &runtime = reject->runtime();
auto throwable = jni::getJavaExceptionForCppException(exception);
auto jsError = convertThrowableToJSError(runtime, throwable);
jsError.value().asObject(runtime).setProperty(runtime, "stack", jsStack);
reject->callback().call(
runtime,
jsError.value());
}

} // namespace

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

std::shared_ptr<std::string> jsStack = std::make_shared<std::string>(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);
jsi::Function rejectInternalJSIFn =
promiseConstructorArgs[1].getObject(runtime).getFunction(
runtime);
auto rejectInternalWeakWrapper =
CallbackWrapper::createWeak(std::move(rejectInternalJSIFn), runtime, jsInvoker_);
Copy link
Member

Choose a reason for hiding this comment

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

This should only be created if traceTurboModulePromiseRejectionEnabled is true

Copy link
Contributor Author

@krystofwoldrich krystofwoldrich Aug 11, 2023

Choose a reason for hiding this comment

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

Even when not tracing the js invocation stack of the Promise, I still need the rejectFn so I can call it on thrown Throwable. I'll try to use one function as you described in a later comment.

krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

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(
std::move(rejectJSIFn), runtime, jsInvoker_, jsStack)
.release();

jclass jPromiseImpl =
Expand All @@ -843,12 +906,15 @@ jsi::Value JavaTurboModule::invokeJavaMethod(

nativeInvoker_->invokeAsync(
[jargs,
rejectInternalWeakWrapper = std::move(rejectInternalWeakWrapper),
jsStack = std::move(jsStack),
globalRefs,
methodID,
instance_ = jni::make_weak(instance_),
moduleNameStr,
methodNameStr,
id = getUniqueId()]() mutable -> void {
auto rejectInternalStrongWrapper = rejectInternalWeakWrapper.lock();
auto instance = instance_.lockLocal();

if (!instance) {
Expand All @@ -871,7 +937,12 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
} catch (...) {
TMPL::asyncMethodCallExecutionFail(
moduleName, methodName, id);
throw;
if (rejectInternalStrongWrapper) {
auto exception = std::current_exception();
rejectWithException(rejectInternalStrongWrapper, exception, *jsStack);
} else {
throw;
}
}

for (auto globalRef : globalRefs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule {
bool isSync,
const char *methodName,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation);
NSMutableArray *retainedObjectsForInvocation,
RCTInternalPromiseRejectBlock optionalInternalRejectBlock);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,24 @@ static int32_t getUniqueId()
return {runtime, std::move(error)};
}

/**
* Creates JSErrorValue with given stack trace.
*/
static jsi::Value createRejectJSErrorValue(jsi::Runtime &runtime, NSDictionary *reason, std::string jsStack)
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
{
jsi::Object cause = convertNSDictionaryToJSIObject(runtime, reason);
jsi::Value error;
NSString *message = reason[@"message"];
if (message) {
error = createJSRuntimeError(runtime, "Exception in HostFunction: " + std::string([message UTF8String]));
} else {
error = createJSRuntimeError(runtime, "Exception in HostFunction: <unknown>");
}
error.asObject(runtime).setProperty(runtime, "stack", jsStack);
error.asObject(runtime).setProperty(runtime, "cause", std::move(cause));
return error;
}

}

jsi::Value ObjCTurboModule::createPromise(jsi::Runtime &runtime, std::string methodName, PromiseInvocationBlock invoke)
Expand All @@ -248,6 +266,9 @@ static int32_t getUniqueId()
}

jsi::Function Promise = runtime.global().getPropertyAsFunction(runtime, "Promise");
std::string jsStack = createJSRuntimeError(runtime, "")
.asObject(runtime).getProperty(runtime, "stack").asString(runtime).utf8(runtime);
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

std::string moduleName = name_;

// Note: the passed invoke() block is not retained by default, so let's retain it here to help keep it longer.
Expand All @@ -257,7 +278,7 @@ static int32_t getUniqueId()
runtime,
jsi::PropNameID::forAscii(runtime, "fn"),
2,
[invokeCopy, jsInvoker = jsInvoker_, moduleName, methodName](
[invokeCopy, jsInvoker = jsInvoker_, moduleName, methodName, jsStack = std::move(jsStack)](
jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) {
std::string moduleMethod = moduleName + "." + methodName + "()";

Expand Down Expand Up @@ -324,7 +345,7 @@ static int32_t getUniqueId()
resolveWasCalled = YES;
};

RCTPromiseRejectBlock rejectBlock = ^(NSString *code, NSString *message, NSError *error) {
auto handlePromiseRejection = ^(NSDictionary *reason) {
if (resolveWasCalled) {
RCTLogError(@"%s: Tried to reject a promise after it's already been resolved.", moduleMethod.c_str());
return;
Expand All @@ -341,17 +362,15 @@ static int32_t getUniqueId()
return;
}

NSDictionary *jsError = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
strongRejectWrapper->jsInvoker().invokeAsync([weakResolveWrapper, weakRejectWrapper, jsError, blockGuard]() {
strongRejectWrapper->jsInvoker().invokeAsync([weakResolveWrapper, weakRejectWrapper, blockGuard, reason, jsStack = std::move(jsStack)]() {
auto strongResolveWrapper2 = weakResolveWrapper.lock();
auto strongRejectWrapper2 = weakRejectWrapper.lock();
if (!strongResolveWrapper2 || !strongRejectWrapper2) {
return;
}

jsi::Runtime &rt = strongRejectWrapper2->runtime();
jsi::Value arg = convertNSDictionaryToJSIObject(rt, jsError);
strongRejectWrapper2->callback().call(rt, arg);
strongRejectWrapper2->callback().call(rt, createRejectJSErrorValue(rt, reason, jsStack));

strongResolveWrapper2->destroy();
strongRejectWrapper2->destroy();
Expand All @@ -361,7 +380,21 @@ static int32_t getUniqueId()
rejectWasCalled = YES;
};

invokeCopy(resolveBlock, rejectBlock);
RCTPromiseRejectBlock rejectBlock = ^(NSString *code, NSString *message, NSError *error) {
NSDictionary *reason = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
handlePromiseRejection(reason);
};

RCTInternalPromiseRejectBlock internalRejectBlock = ^(NSException *exception) {
handlePromiseRejection(@{
@"name": exception.name,
@"message": exception.reason,
@"stackSymbols": exception.callStackSymbols,
@"stackReturnAddresses": exception.callStackReturnAddresses,
});
};
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

invokeCopy(resolveBlock, rejectBlock, internalRejectBlock);
return jsi::Value::undefined();
});

Expand All @@ -382,7 +415,8 @@ static int32_t getUniqueId()
bool isSync,
const char *methodName,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation)
NSMutableArray *retainedObjectsForInvocation,
RCTInternalPromiseRejectBlock optionalInternalRejectBlock)
{
__block id result;
__weak id<RCTBridgeModule> weakModule = instance_;
Expand All @@ -405,7 +439,16 @@ static int32_t getUniqueId()
@try {
[inv invokeWithTarget:strongModule];
} @catch (NSException *exception) {
throw convertNSExceptionToJSError(runtime, exception);
if (isSync) {
throw convertNSExceptionToJSError(runtime, exception);
} else {
if (optionalInternalRejectBlock != nil) {
optionalInternalRejectBlock(exception);
} else {
// JSFunction returning void
throw;
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
}
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
} @finally {
[retainedObjectsForInvocation removeAllObjects];
}
Expand Down Expand Up @@ -718,20 +761,21 @@ static int32_t getUniqueId()

if (returnType == PromiseKind) {
returnValue = createPromise(
runtime, methodNameStr, ^(RCTPromiseResolveBlock resolveBlock, RCTPromiseRejectBlock rejectBlock) {
runtime, methodNameStr, ^(RCTPromiseResolveBlock resolveBlock, RCTPromiseRejectBlock rejectBlock, RCTInternalPromiseRejectBlock internalRejectBlock) {
RCTPromiseResolveBlock resolveCopy = [resolveBlock copy];
RCTPromiseRejectBlock rejectCopy = [rejectBlock copy];
RCTInternalPromiseRejectBlock internalRejectCopy = [internalRejectBlock copy];

[inv setArgument:(void *)&resolveCopy atIndex:count + 2];
[inv setArgument:(void *)&rejectCopy atIndex:count + 3];
[retainedObjectsForInvocation addObject:resolveCopy];
[retainedObjectsForInvocation addObject:rejectCopy];
// The return type becomes void in the ObjC side.
performMethodInvocation(runtime, isMethodSync(VoidKind), methodName, inv, retainedObjectsForInvocation);
performMethodInvocation(runtime, isMethodSync(VoidKind), methodName, inv, retainedObjectsForInvocation, internalRejectCopy);
});
} else {
id result =
performMethodInvocation(runtime, isMethodSync(returnType), methodName, inv, retainedObjectsForInvocation);
performMethodInvocation(runtime, isMethodSync(returnType), methodName, inv, retainedObjectsForInvocation, nil);

if (isMethodSync(returnType)) {
TurboModulePerfLogger::syncMethodCallReturnConversionStart(moduleName, methodName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,9 @@ class SampleTurboModuleExample extends React.Component<{||}, State> {
}
},
promiseThrows: () => {
try {
// $FlowFixMe[unused-promise]
NativeSampleTurboModule.promiseThrows?.();
} catch (e) {
return e.message;
}
NativeSampleTurboModule.promiseThrows?.()
.then(() => {})
.catch(e => this._setResult('promiseThrows', e.message));
},
voidFuncAssert: () => {
try {
Expand All @@ -115,8 +112,9 @@ class SampleTurboModuleExample extends React.Component<{||}, State> {
},
promiseAssert: () => {
try {
// $FlowFixMe[unused-promise]
NativeSampleTurboModule.promiseAssert?.();
NativeSampleTurboModule.promiseAssert?.()
.then(() => {})
.catch(e => this._setResult('promiseAssert', e.message));
} catch (e) {
return e.message;
}
Expand Down