Skip to content

Async ClusterPubSub.subscribe() raises KeyError on an uninitialized RedisCluster client #4296

Description

@Classic298

Version: redis-py 8.0.1, code unchanged in 8.1.0. No server involved, the failure happens before any connection attempt.

Platform: Python 3.12 on Windows, also hit in production with Python 3.11 on Linux (Docker).

Description:

RedisCluster.execute_command lazily initializes the client on first use. The async ClusterPubSub.execute_command does not: for a subscribe it hashes the channel and looks the slot up in nodes_manager.slots_cache directly. On a client that has not executed any other command yet the slot cache is still an empty dict, so the lookup raises a bare KeyError carrying the slot number.

A second defect hides the intended error type: get_node_from_slot converts failed lookups to SlotNotCoveredError, but its except clause only catches IndexError and TypeError. Since slots_cache is a dict, a missing slot raises KeyError and skips the conversion, leaking an internal error to callers.

Minimal repro, no server needed:

import asyncio
from redis.asyncio.cluster import RedisCluster


async def main():
    rc = RedisCluster.from_url("redis://127.0.0.1:9999/0")
    await rc.pubsub().subscribe("some-channel")


asyncio.run(main())
Traceback (most recent call last):
  ...
  File "redis/asyncio/client.py", line 1386, in subscribe
    ret_val = await self.execute_command("SUBSCRIBE", *new_channels.keys())
  File "redis/asyncio/cluster.py", line 3978, in execute_command
    node = self.cluster.nodes_manager.get_node_from_slot(
  File "redis/asyncio/cluster.py", line 1951, in get_node_from_slot
    if len(self.slots_cache[slot]) > 1 and load_balancing_strategy:
           ~~~~~~~~~~~~~~~~^^^^^^
KeyError: 11554

Any application whose first operation on a fresh async cluster client is a pubsub subscribe hits this deterministically, against a perfectly healthy cluster. Open WebUI starts its pubsub listener right after creating the client, so every Redis Cluster deployment fails this way on startup (open-webui/open-webui#29138). Running any regular command first (for example ping) works around it, because that path runs initialize() and populates the slot cache.

Expected behavior: ClusterPubSub.execute_command initializes the cluster client before the slot lookup, the same way RedisCluster.execute_command does. Independently, get_node_from_slot should catch KeyError alongside IndexError and TypeError so an uncovered slot surfaces as SlotNotCoveredError.

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