{"id":"592b7d9c2c5d9b4a","repo":"redis/redis-py","slug":"cluster-client-has-no-nodes-cannot-create-health","errorCode":null,"errorMessage":"Cluster client has no nodes - cannot create health check client","messagePattern":"Cluster client has no nodes - cannot create health check client","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"redis/asyncio/multidb/healthcheck.py","lineNumber":209,"sourceCode":"                    # different names (``_require_full_coverage`` vs\n                    # ``require_full_coverage``), so resolve it defensively to\n                    # support a sync RedisCluster underlying client too.\n                    require_full_coverage = getattr(\n                        nodes_manager,\n                        \"require_full_coverage\",\n                        getattr(nodes_manager, \"_require_full_coverage\", True),\n                    )\n                    client = AsyncRedisCluster(\n                        host=first_node.host,\n                        port=first_node.port,\n                        dynamic_startup_nodes=nodes_manager._dynamic_startup_nodes,\n                        address_remap=nodes_manager.address_remap,\n                        require_full_coverage=require_full_coverage,\n                        retry=database.client.retry,\n                        **filtered_kwargs,\n                    )\n                else:\n                    raise ValueError(\n                        \"Cluster client has no nodes - cannot create health check client\"\n                    )\n            else:\n                raise TypeError(f\"Unsupported client type: {type(database.client)}\")\n            self._clients[db_id] = client\n\n        return client\n\n    async def close(self) -> None:\n        \"\"\"Close all health check clients.\"\"\"\n        close_tasks = [\n            asyncio.create_task(client.aclose()) for client in self._clients.values()\n        ]\n\n        if close_tasks:\n            await asyncio.gather(*close_tasks, return_exceptions=True)\n\n        self._clients.clear()","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/multidb/healthcheck.py#L191-L227","documentation":"Raised by `AbstractHealthCheckPolicy.get_client()` (redis/asyncio/multidb/healthcheck.py:209) when the active database wraps a cluster client (`AsyncRedisCluster`/`SyncRedisCluster`) but `database.client.startup_nodes` is empty. Without a startup node the policy cannot construct a health-check `AsyncRedisCluster`, so it aborts with ValueError before attempting any probe.","triggerScenarios":"A DatabaseConfig whose client is a RedisCluster constructed without any startup nodes (e.g. `RedisCluster()` with no `host`/`startup_nodes`), then triggering the first health check which calls `get_client()` for that database.","commonSituations":"Misconstructed RedisCluster — passing only `cluster_kwargs` without a host or startup_nodes; cluster client whose node list was cleared; copy/paste config error omitting the endpoint.","solutions":["Ensure the RedisCluster client for that database is constructed with at least one node: `RedisCluster(host='...', port=16379)` or `startup_nodes=[ClusterNode(...)]`.","Validate `len(client.startup_nodes) > 0` before adding the database via `add_database()` or in `DatabaseConfig`.","If using `from_url`, supply a valid `redis://host:port` cluster URL."],"exampleFix":"# before\ncluster = RedisCluster()  # no nodes\nclient = MultiDBClient(MultiDbConfig(\n    databases_config=[DatabaseConfig(client_kwargs={'__init__': {}})],\n    client_class=RedisCluster,\n))\n\n# after\ncluster = RedisCluster(host='cluster-host', port=16379)\n# or via DatabaseConfig.from_url='redis://cluster-host:16379/0'\nclient = MultiDBClient(MultiDbConfig(\n    databases_config=[DatabaseConfig(from_url='redis://cluster-host:16379/0')],\n    client_class=RedisCluster,\n))","handlingStrategy":"validation","validationCode":"def cluster_has_nodes(database) -> bool:\n    client = database.client\n    return bool(getattr(client, 'startup_nodes', None))\n\n# validate before adding the database / triggering health checks","typeGuard":"import redis.asyncio as aioredis\n\ndef is_cluster_with_nodes(database) -> bool:\n    c = database.client\n    return isinstance(c, (aioredis.RedisCluster,)) and bool(getattr(c, 'startup_nodes', None))","tryCatchPattern":"try:\n    await client.initialize()\nexcept ValueError as e:\n    if 'no nodes' in str(e):\n        # fix DatabaseConfig to include a host/startup_nodes, then re-init\n        ...\n    raise","preventionTips":["Always construct RedisCluster with `host=`/`port=` or `startup_nodes=[...]`.","Validate `len(client.startup_nodes) > 0` before wrapping a cluster client in MultiDBClient.","In tests, point the cluster client at a real testcontainer cluster, not an empty constructor."],"tags":["multidb","cluster","health","configuration"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}