Skip to content

Querying calls triggers unintended side effects: call.leave() invoked on detached instances #1366

@amaslanka

Description

@amaslanka

Describe the bug
Querying a call using queryCalls() (or get() method in the Call objects) results in a side effect where the SDK automatically invokes internal call state update logic. Specifically, this leads to invoking updateFromResponse()updateRingingState(), which checks if the call has been rejected (rejectedBy.size == 1) and no active outgoing members (outgoingMemberCount == 0). If these conditions are met, the SDK incorrectly calls call.leave(), which cleans up the call state and terminates the call screen, even if the call was simply queried for other purposes.

This is especially problematic because:

  • The queried Call instance is expected to be detached and passive.
  • However, invoking queryCalls() creates a full Call object that mutates the shared singleton StreamVideoClient state.
  • As a result, calling leave() on this newly queried object affects global state: it disables the microphone and camera, stops CallService, and removes the ringing call from memory.

Due to this, it becomes impossible to re-join a previously rejected call, as fetching it forcibly triggers termination logic.

Additional context

  • Our use case requires querying calls in order to manage group calling behavior within chat rooms. Specifically:
    • We want to detect if there's already an active call in a group chat before starting a new one.
    • If another user taps the Start Call button while a call is ongoing, they should join the existing session instead of creating a parallel one.
    • This logic is necessary to avoid race conditions and ensure a single source of truth for the ongoing call.
    • However, due to the current SDK behavior, querying a call causes SDK-internal state changes, which undermines this logic and breaks active sessions.

SDK version

  • 1.4.4

To Reproduce
Steps to reproduce the behavior:

  1. Start a call between three users (with unique id and ring = true)
  2. User A starts the call, user B accepts the call, user C rejects the call.
  3. User C tries to query the same call using queryCalls().

Image
4. If an ongoing call is found, its id is passed to the StreamVideoActivity.
5. This causes a race condition, because while we invoke the StreamVideoActivity, the previous Call object fetched using queryCall also updates the internal state of the StreamVideoClient, which is a singleton.
6. The StreamVideoClient enters unexpected and undesired state -> when user C tries to rej-oin the call, the UI closes unexpectedly, because call.leave() is called on the Call object fetched via queryCalls.
7. The above bug causes that the call connection leaks and even if the StreamVideoActivity is closed, User C is still visible in the call for other users. Each attempt of joining the same call results in a new "instance" of User C being present in the call.

Expected behavior
Querying a call using queryCalls() or getCall() should not have side effects. It should return a detached Call object that does not modify the global StreamVideoClient state unless explicitly joined or modified.

Device:

  • Vendor and model: Google Pixel 6
  • Android version: 15

Screenshots

  • Internal logic tracing to updateRingingState() and call.leave()
    Image

  • Call instance destructively mutating StreamVideoClient

Image

Additional context

  • The internal getCall() method in StreamVideoClient (which is safe and side-effect free) is not publicly accessible.
  • We suggest exposing a safe, detached version of getCall() for UI use cases that don’t require active state mutation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions