SDK versions
- Observed with
reown-swift 2.2.9
- The relevant implementation appears unchanged in 2.3.0:
Sources/WalletConnectRelay/RelayClient.swift, waitForSubscriptionResponse (roughly lines 226–272)
Crash
We have a production iOS crash in RelayClient.waitForSubscriptionResponse where a checked continuation is resumed more than once. Crashlytics grouped 4 fatal events over the last 89 days (2 in the last 7 days).
The failure is consistent with:
SWIFT TASK CONTINUATION MISUSE: waitForSubscriptionResponse(topic:) tried to resume its continuation more than once
Suspected race
waitForSubscriptionResponse installs a Combine sink inside withCheckedThrowingContinuation:
receiveCompletion(.failure(...)) resumes the continuation by throwing (including the timeout path).
receiveValue resumes the same continuation by returning the subscription result.
- Each path cancels the cancellable, but cancellation does not serialize two callbacks that are already in flight.
If the subscription response arrives at the timeout boundary, the response and timeout/completion callbacks can both pass through and resume the continuation.
Expected behavior
Only the first terminal callback should resume the continuation. Later response/completion callbacks should be ignored.
Suggested fix
Add a resume-once gate shared by receiveCompletion and receiveValue (for example, a small lock/actor-backed state machine), and route every continuation resume through it. Cancelling the subscription can remain cleanup, but should not be the synchronization mechanism.
A regression test could deliberately schedule a subscription response at the timeout boundary, repeat the race many times, and assert that the async operation completes exactly once with either the response or timeout result.
I can provide additional symbolicated stack details if helpful.
SDK versions
reown-swift2.2.9Sources/WalletConnectRelay/RelayClient.swift,waitForSubscriptionResponse(roughly lines 226–272)Crash
We have a production iOS crash in
RelayClient.waitForSubscriptionResponsewhere a checked continuation is resumed more than once. Crashlytics grouped 4 fatal events over the last 89 days (2 in the last 7 days).The failure is consistent with:
Suspected race
waitForSubscriptionResponseinstalls a Combine sink insidewithCheckedThrowingContinuation:receiveCompletion(.failure(...))resumes the continuation by throwing (including the timeout path).receiveValueresumes the same continuation by returning the subscription result.If the subscription response arrives at the timeout boundary, the response and timeout/completion callbacks can both pass through and resume the continuation.
Expected behavior
Only the first terminal callback should resume the continuation. Later response/completion callbacks should be ignored.
Suggested fix
Add a resume-once gate shared by
receiveCompletionandreceiveValue(for example, a small lock/actor-backed state machine), and route every continuation resume through it. Cancelling the subscription can remain cleanup, but should not be the synchronization mechanism.A regression test could deliberately schedule a subscription response at the timeout boundary, repeat the race many times, and assert that the async operation completes exactly once with either the response or timeout result.
I can provide additional symbolicated stack details if helpful.