Describe the bug
On Android, getThreatIdentifiers() and getRaspExecutionStateIdentifiers() resolve a statically cached WritableNativeArray (ThreatEvent.ALL_EVENTS / RaspExecutionStateEvent.ALL_EVENTS, defined as companion object val). A WritableNativeArray can only cross the React Native bridge once, after it is serialized it is flagged as consumed. Because the array lives for the whole process lifetime while the React context is re-created on every JS reload, the second resolve of that same instance throws:
Exception in HostFunction: Array already consumed
com.facebook.react.bridge.ObjectAlreadyConsumedException: Array already consumed
It surfaces as an unhandled promise rejection coming from FreeraspReactNativeModule.getThreatIdentifiers.
Root cause (same code in 4.5.2 and 5.0.0):
// events/ThreatEvent.kt - process-lifetime cached WritableNativeArray
companion object {
internal val ALL_EVENTS = Arguments.fromList(
listOf(AppIntegrity, PrivilegedAccess, Debug, /* ... */ Automation).map { it.value }
)
}
// FreeraspReactNativeModule.kt - resolves the SAME instance every call
@ReactMethod
fun getThreatIdentifiers(promise: Promise) {
promise.resolve(ThreatEvent.ALL_EVENTS)
}
@ReactMethod
fun getRaspExecutionStateIdentifiers(promise: Promise) {
promise.resolve(RaspExecutionStateEvent.ALL_EVENTS)
}
Arguments.fromList(...) returns a WritableNativeArray (single-use across the bridge). The JS side (useFreeRasp → getThreatIdentifiers() in channels/threat.ts) runs on every React-context init:
- 1st init: resolves
ALL_EVENTS → consumed (OK).
- 2nd init in the same process (reload/restart): resolves the already-consumed
ALL_EVENTS → throws.
@ReactMethod + ReactContextBaseJavaModule route through legacy-bridge interop even under the New Architecture, so Arguments.fromList yields a WritableNativeArray regardless.
To Reproduce
- Integrate
freerasp-react-native and start it via useFreeRasp(config, actions).
- Build & run a debug Android app; let freeRASP initialize.
- Trigger a JS bundle reload (Dev Menu → Reload, or press
r in Metro). Anything that re-creates the React context within the same process.
- The unhandled promise rejection/red box appears.
The real trigger is "the RN bridge/context is initialized a second time in the same OS process," so it can also occur in production via react-native-restart, reload-after-update flows, or certain activity/bridge recreation paths, not strictly dev-only.
Expected behavior
getThreatIdentifiers() / getRaspExecutionStateIdentifiers() resolve successfully on every call, including after a JS reload or any second React-context initialization in the same process.
Screenshots
Full stack trace (logcat):
W ReactNativeJS: 'Unhandled promise rejection!', 0, { [Error: Exception in HostFunction: Array already consumed]
cause:
{ name: 'com.facebook.react.bridge.ObjectAlreadyConsumedException',
message: 'Array already consumed',
stackElements:
[ { className: 'com.facebook.react.bridge.WritableNativeArray', fileName: 'WritableNativeArray.kt', lineNumber: -2, methodName: 'pushNativeArray' },
{ className: 'com.facebook.react.bridge.WritableNativeArray', fileName: 'WritableNativeArray.kt', lineNumber: 38, methodName: 'pushArray' },
{ className: 'com.facebook.react.bridge.Arguments', fileName: 'Arguments.kt', lineNumber: 158, methodName: 'fromJavaArgs' },
{ className: 'com.facebook.react.bridge.CxxCallbackImpl', fileName: 'CxxCallbackImpl.kt', lineNumber: 18, methodName: 'invoke' },
{ className: 'com.facebook.react.bridge.PromiseImpl', fileName: 'PromiseImpl.kt', lineNumber: 29, methodName: 'resolve' },
{ className: 'com.freeraspreactnative.FreeraspReactNativeModule', fileName: 'FreeraspReactNativeModule.kt', lineNumber: 119, methodName: 'getThreatIdentifiers' },
{ className: 'com.facebook.jni.NativeRunnable', methodName: 'run' } ] } }
Please complete the following information:
- Device: Android (any); reproduced on a Pixel 9 emulator
- OS version: Android 14
- Version of freeRASP: 4.5.2 (confirmed); same code present in 5.0.0 (latest)
Additional context
- React Native 0.83.6, New Architecture enabled (
newArchEnabled=true), Expo SDK 55. Android only — iOS has no WritableNativeArray consume semantics.
Suggested fix: cache the plain values, build a fresh WritableArray per call so each resolve gets its own consumable instance:
// events/ThreatEvent.kt
companion object {
internal val ALL_EVENT_IDENTIFIERS: List<Int> =
listOf(AppIntegrity, PrivilegedAccess, Debug, /* ... */ Automation).map { it.value }
}
// FreeraspReactNativeModule.kt
@ReactMethod
fun getThreatIdentifiers(promise: Promise) {
promise.resolve(Arguments.fromList(ThreatEvent.ALL_EVENT_IDENTIFIERS))
}
@ReactMethod
fun getRaspExecutionStateIdentifiers(promise: Promise) {
promise.resolve(Arguments.fromList(RaspExecutionStateEvent.ALL_EVENT_IDENTIFIERS))
}
Equivalent: Arguments.fromList(ALL_EVENTS.toArrayList()) per call. Same pattern applies to any other @ReactMethod that resolves a cached WritableArray/WritableMap.
Consumer workaround until released: patch-package on FreeraspReactNativeModule.kt, swapping the two promise.resolve(...ALL_EVENTS) lines for the fresh-array variant.
Describe the bug
On Android,
getThreatIdentifiers()andgetRaspExecutionStateIdentifiers()resolve a statically cachedWritableNativeArray(ThreatEvent.ALL_EVENTS/RaspExecutionStateEvent.ALL_EVENTS, defined ascompanion object val). AWritableNativeArraycan only cross the React Native bridge once, after it is serialized it is flagged as consumed. Because the array lives for the whole process lifetime while the React context is re-created on every JS reload, the second resolve of that same instance throws:It surfaces as an unhandled promise rejection coming from
FreeraspReactNativeModule.getThreatIdentifiers.Root cause (same code in 4.5.2 and 5.0.0):
Arguments.fromList(...)returns aWritableNativeArray(single-use across the bridge). The JS side (useFreeRasp→getThreatIdentifiers()inchannels/threat.ts) runs on every React-context init:ALL_EVENTS→ consumed (OK).ALL_EVENTS→ throws.@ReactMethod+ReactContextBaseJavaModuleroute through legacy-bridge interop even under the New Architecture, soArguments.fromListyields aWritableNativeArrayregardless.To Reproduce
freerasp-react-nativeand start it viauseFreeRasp(config, actions).rin Metro). Anything that re-creates the React context within the same process.The real trigger is "the RN bridge/context is initialized a second time in the same OS process," so it can also occur in production via
react-native-restart, reload-after-update flows, or certain activity/bridge recreation paths, not strictly dev-only.Expected behavior
getThreatIdentifiers()/getRaspExecutionStateIdentifiers()resolve successfully on every call, including after a JS reload or any second React-context initialization in the same process.Screenshots
Full stack trace (logcat):
Please complete the following information:
Additional context
newArchEnabled=true), Expo SDK 55. Android only — iOS has noWritableNativeArrayconsume semantics.Suggested fix: cache the plain values, build a fresh
WritableArrayper call so each resolve gets its own consumable instance:Equivalent:
Arguments.fromList(ALL_EVENTS.toArrayList())per call. Same pattern applies to any other@ReactMethodthat resolves a cachedWritableArray/WritableMap.Consumer workaround until released:
patch-packageonFreeraspReactNativeModule.kt, swapping the twopromise.resolve(...ALL_EVENTS)lines for the fresh-array variant.