What happened?
Hi,
we are writing a plugin for handling travel rights in public transport. As part of this work, we manage one or more key-pairs which are used to authenticate against the backend. The key-pair has a rather complex lifecycle. Master state is determined by the server.
As part of this work we are providing apps with a plugin method which allows them to easily interact with the backend:
const result = await clientRef.current?.remoteClientCallHandler<string>({
tokenContextId: DEFAULT_TOKEN_CONTEXT_ID,
tokenEncodingRequest,
fn: async (params) => {
// custom work there
}
})
where fn is
export type TravellerClientCall<R> = (input: RemoteClientCallInput) => Promise<R>
and params is some values we need the client to forward in order to be authenticated, i.e.
export type RemoteClientCallInput = {
tokenId: string
secureContainerToken: string
attestation: Attestation | undefined
correlationId: string
}
So the async JS-function fn is essentially passed to native code, then invoked from an executor thread in Android.
In native (Java) code, invoking fn returns a Promise<Promise<MetadataResult>> where MetadataResult accounts for various expected throwable errors. This works as expected, i.e. first waiting for Promise<MetadataResult> and then MetadataResult.
The problem arises when we want to invoke fn twice from a single TravellerClientCall when the first fn-invocation throws an error, the second fn-invocation hangs forever. To be more precise, awaiting MetadataResult hangs on the second invocation of fn.
However the fn JS code actually executes a second time (returns a value, does not throw). And the hang is not happening if we insert an await sleep(1 millisecond) in the code path which runs on the second invocation. So this indicates some kind of race condition / signalling / threading problem. To be clear, in the unit test, the fn returns almost immidiately. Everything is async.
So what we are wondering is whether this approach is expected to work and/or any obious solution we might have missed, and how to continue from here - I have already tried everything I can think of.
Reproduceable Code
Relevant log output
A thread dump indicates
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W "default-idleing-pool-thread-1" state=WAITING
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at jdk.internal.misc.Unsafe.park(Native Method)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at java.util.concurrent.locks.LockSupport.park(LockSupport.java:226)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1840)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3797)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3738)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1874)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2048)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP com.abtmobileclientsdkexample W at com.margelo.nitro.abt.JavaHybridAbtMobileClientSDK.lambda$remoteClientCallHandlerImpl$7(JavaHybridAbtMobileClientSDK.java:640)
Device
Android 17 Emulator
Nitro Modules Version
0.36.1
Nitrogen Version
0.36.1
Can you reproduce this issue in the Nitro Example app here?
I didn't try (⚠️ your issue might get ignored & closed if you don't try this)
Additional information
What happened?
Hi,
we are writing a plugin for handling travel rights in public transport. As part of this work, we manage one or more key-pairs which are used to authenticate against the backend. The key-pair has a rather complex lifecycle. Master state is determined by the server.
As part of this work we are providing apps with a plugin method which allows them to easily interact with the backend:
where
fnisand
paramsis some values we need the client to forward in order to be authenticated, i.e.So the async JS-function
fnis essentially passed to native code, then invoked from an executor thread in Android.In native (Java) code, invoking
fnreturns aPromise<Promise<MetadataResult>>whereMetadataResultaccounts for various expected throwable errors. This works as expected, i.e. first waiting forPromise<MetadataResult>and thenMetadataResult.The problem arises when we want to invoke
fntwice from a singleTravellerClientCallwhen the firstfn-invocation throws an error, the secondfn-invocation hangs forever. To be more precise, awaitingMetadataResulthangs on the second invocation offn.However the
fnJS code actually executes a second time (returns a value, does not throw). And the hang is not happening if we insert anawait sleep(1 millisecond)in the code path which runs on the second invocation. So this indicates some kind of race condition / signalling / threading problem. To be clear, in the unit test, thefnreturns almost immidiately. Everything is async.So what we are wondering is whether this approach is expected to work and/or any obious solution we might have missed, and how to continue from here - I have already tried everything I can think of.
Reproduceable Code
-Relevant log output
Device
Android 17 Emulator
Nitro Modules Version
0.36.1
Nitrogen Version
0.36.1
Can you reproduce this issue in the Nitro Example app here?
I didn't try (⚠️ your issue might get ignored & closed if you don't try this)
Additional information