Skip to content

Bug: Race condition in supabase.removeChannel() causes it to incorrectly remove all channels #487

Open
@IdrisCelik

Description

@IdrisCelik

Describe the bug

The supabase.removeChannel(channel) method can, under certain conditions, incorrectly remove all channels from the client's internal list instead of just the single specified channel.

This behavior is intermittent and appears to be caused by a race condition. The outcome depends on whether the target channel has fully completed its subscription handshake with the server before removeChannel is called. The bug is caused by a redundant and faulty filter operation within the removeChannel method itself.

To Reproduce

The easiest way to consistently reproduce the bug is to create channels without subscribing them, which guarantees the faulty logic will fail.

  1. Create a Supabase client instance.
  2. Create multiple channels using supabase.channel().
  3. Log the current channels to see the initial state (3 channels).
  4. Call supabase.removeChannel() on one of the channels.
  5. Log the channels again and observe the error.
// Minimal code to reproduce the issue
const channel1 = supabase.channel('room1');
const channel2 = supabase.channel('room2');
const channel3 = supabase.channel('room3');

// 1. Check initial state
console.log('Channels before removal:', supabase.getChannels()); 
// EXPECTED AND ACTUAL: Shows an array of 3 channel objects.

// 2. Attempt to remove just one channel
const status = await supabase.removeChannel(channel2);
console.log('Removal status:', status); // "ok" (sometimes returns undefined but that is a separate issue, might PR if I got the time.)

// 3. Check final state
console.log('Channels after removal:', supabase.getChannels());
// EXPECTED: An array with [channel1, channel3]
// ACTUAL: An empty array []

Expected behavior

I expect only the channel passed to supabase.removeChannel() (in this case, channel2) to be removed from the list of active channels. The final console.log should show an array containing channel1 and channel3.

System information

  • OS: Platform-agnostic (reproducible on macOS, Windows, Linux)
  • Browser (if applies): Platform-agnostic (reproducible in Chrome, Firefox, and Node.js environments)
  • Version of supabase-js: latest ^2.50.0
  • Version of Node.js: Any (e.g., 18.x)

Why it "Sometimes" Happens (The Race Condition)

The bug stems from this line in the RealtimeClient.ts removeChannel method:
this.channels = this.channels.filter((c) => c._joinRef !== channel._joinRef)

The property _joinRef is only assigned a value after a channel successfully subscribes to the server. Before that, it's undefined.

  • Scenario A (It Fails): If you call removeChannel(channel2) before channel2 has finished subscribing, its _joinRef is undefined. The filter then compares c._joinRef !== undefined for every other unsubscribed channel. This becomes undefined !== undefined, which is false, causing all unsubscribed channels to be removed.

  • Scenario B (It Works by Coincidence): If you call removeChannel(channel2) after it has had time to subscribe, its _joinRef will have a value (e.g., "1"). The filter then becomes c._joinRef !== "1". This works as expected, because other channels will either have a different _joinRef or undefined, both of which satisfy the condition.

The Deeper Issue & The Fix

The root cause is that the channel.unsubscribe() method (which is called inside removeChannel) already does the job of correctly removing the channel from the client's list. The extra, faulty filter line in removeChannel is redundant and is what introduces the race condition.

I am going to make a PR later this day.

Also feel free to checkout and merge my other PR, completely free of change 😜 supabase/cli#3720

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