Skip to content

RelayClient.waitForSubscriptionResponse can resume its continuation twice when timeout races a response #339

Description

@sergiikhliustin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions