From fe17d7a321f74020de72ce86a70ce13faf7cb889 Mon Sep 17 00:00:00 2001 From: Petya Slavova Date: Mon, 8 Dec 2025 14:33:49 +0200 Subject: [PATCH 1/2] PubSub client health check handling for sync client implementation --- redis/client.py | 20 ++++++++-- tests/test_asyncio/test_pubsub.py | 57 +++++++++++++++++++++++++++++ tests/test_pubsub.py | 61 +++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/redis/client.py b/redis/client.py index d3ab3cfcfe..917947b185 100755 --- a/redis/client.py +++ b/redis/client.py @@ -1009,10 +1009,22 @@ def is_health_check_response(self, response) -> bool: If there are no subscriptions redis responds to PING command with a bulk response, instead of a multi-bulk with "pong" and the response. """ - return response in [ - self.health_check_response, # If there was a subscription - self.health_check_response_b, # If there wasn't - ] + if self.encoder.decode_responses: + return ( + response + in [ + self.health_check_response, # If there is a subscription + self.HEALTH_CHECK_MESSAGE, # If there is no subscriptions and decode_responses=True + ] + ) + else: + return ( + response + in [ + self.health_check_response, # If there is a subscription + self.health_check_response_b, # If there isn't a subscription and decode_responses=False + ] + ) def check_health(self) -> None: conn = self.connection diff --git a/tests/test_asyncio/test_pubsub.py b/tests/test_asyncio/test_pubsub.py index b281cb1281..47bccf1b1b 100644 --- a/tests/test_asyncio/test_pubsub.py +++ b/tests/test_asyncio/test_pubsub.py @@ -671,6 +671,63 @@ async def test_send_pubsub_ping_message(self, r: redis.Redis): await p.aclose() +@pytest.mark.onlynoncluster +class TestPubSubHealthCheckResponse: + """Tests for health check response validation with different decode_responses settings""" + + async def test_health_check_response_decode_false_list_format(self, r: redis.Redis): + """Test health_check_response includes list format with decode_responses=False""" + p = r.pubsub() + # List format: [b"pong", b"redis-py-health-check"] + assert [b"pong", b"redis-py-health-check"] in p.health_check_response + await p.aclose() + + async def test_health_check_response_decode_false_bytes_format( + self, r: redis.Redis + ): + """Test health_check_response includes bytes format with decode_responses=False""" + p = r.pubsub() + # Bytes format: b"redis-py-health-check" + assert b"redis-py-health-check" in p.health_check_response + await p.aclose() + + async def test_health_check_response_decode_true_list_format(self, create_redis): + """Test health_check_response includes list format with decode_responses=True""" + r = await create_redis(decode_responses=True) + p = r.pubsub() + # List format: ["pong", "redis-py-health-check"] + assert ["pong", "redis-py-health-check"] in p.health_check_response + await p.aclose() + await r.aclose() + + async def test_health_check_response_decode_true_string_format(self, create_redis): + """Test health_check_response includes string format with decode_responses=True""" + r = await create_redis(decode_responses=True) + p = r.pubsub() + # String format: "redis-py-health-check" (THE FIX!) + assert "redis-py-health-check" in p.health_check_response + await p.aclose() + await r.aclose() + + async def test_health_check_response_decode_false_excludes_string( + self, r: redis.Redis + ): + """Test health_check_response excludes string format with decode_responses=False""" + p = r.pubsub() + # String format should NOT be in the list when decode_responses=False + assert "redis-py-health-check" not in p.health_check_response + await p.aclose() + + async def test_health_check_response_decode_true_excludes_bytes(self, create_redis): + """Test health_check_response excludes bytes format with decode_responses=True""" + r = await create_redis(decode_responses=True) + p = r.pubsub() + # Bytes format should NOT be in the list when decode_responses=True + assert b"redis-py-health-check" not in p.health_check_response + await p.aclose() + await r.aclose() + + @pytest.mark.onlynoncluster class TestPubSubConnectionKilled: @skip_if_server_version_lt("3.0.0") diff --git a/tests/test_pubsub.py b/tests/test_pubsub.py index db313e2437..4a49ab4dce 100644 --- a/tests/test_pubsub.py +++ b/tests/test_pubsub.py @@ -892,6 +892,67 @@ def test_send_pubsub_ping_message(self, r): ) +@pytest.mark.onlynoncluster +class TestPubSubHealthCheckResponse: + """Tests for health check response validation with different decode_responses settings""" + + def test_is_health_check_response_decode_false_list_format(self, r): + """Test is_health_check_response recognizes list format with decode_responses=False""" + p = r.pubsub() + # List format: [b"pong", b"redis-py-health-check"] + assert p.is_health_check_response([b"pong", b"redis-py-health-check"]) + + def test_is_health_check_response_decode_false_bytes_format(self, r): + """Test is_health_check_response recognizes bytes format with decode_responses=False""" + p = r.pubsub() + # Bytes format: b"redis-py-health-check" + assert p.is_health_check_response(b"redis-py-health-check") + + def test_is_health_check_response_decode_false_rejects_string(self, r): + """Test is_health_check_response rejects string format with decode_responses=False""" + p = r.pubsub() + # String format should NOT be recognized when decode_responses=False + assert not p.is_health_check_response("redis-py-health-check") + + def test_is_health_check_response_decode_true_list_format(self, request): + """Test is_health_check_response recognizes list format with decode_responses=True""" + r = _get_client(redis.Redis, request, decode_responses=True) + p = r.pubsub() + # List format: ["pong", "redis-py-health-check"] + assert p.is_health_check_response(["pong", "redis-py-health-check"]) + + def test_is_health_check_response_decode_true_string_format(self, request): + """Test is_health_check_response recognizes string format with decode_responses=True""" + r = _get_client(redis.Redis, request, decode_responses=True) + p = r.pubsub() + # String format: "redis-py-health-check" (THE FIX!) + assert p.is_health_check_response("redis-py-health-check") + + def test_is_health_check_response_decode_true_rejects_bytes(self, request): + """Test is_health_check_response rejects bytes format with decode_responses=True""" + r = _get_client(redis.Redis, request, decode_responses=True) + p = r.pubsub() + # Bytes format should NOT be recognized when decode_responses=True + assert not p.is_health_check_response(b"redis-py-health-check") + + def test_is_health_check_response_decode_true_rejects_invalid(self, request): + """Test is_health_check_response rejects invalid responses with decode_responses=True""" + r = _get_client(redis.Redis, request, decode_responses=True) + p = r.pubsub() + # Invalid responses should be rejected + assert not p.is_health_check_response("invalid-response") + assert not p.is_health_check_response(["pong", "invalid-response"]) + assert not p.is_health_check_response(None) + + def test_is_health_check_response_decode_false_rejects_invalid(self, r): + """Test is_health_check_response rejects invalid responses with decode_responses=False""" + p = r.pubsub() + # Invalid responses should be rejected + assert not p.is_health_check_response(b"invalid-response") + assert not p.is_health_check_response([b"pong", b"invalid-response"]) + assert not p.is_health_check_response(None) + + @pytest.mark.onlynoncluster class TestPubSubConnectionKilled: @skip_if_server_version_lt("3.0.0") From e2ca9832ca13edb603c93d71d7a49ef5d891bcce Mon Sep 17 00:00:00 2001 From: petyaslavova Date: Mon, 8 Dec 2025 15:10:06 +0200 Subject: [PATCH 2/2] Update redis/client.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- redis/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis/client.py b/redis/client.py index 917947b185..e0d62e5216 100755 --- a/redis/client.py +++ b/redis/client.py @@ -1014,7 +1014,7 @@ def is_health_check_response(self, response) -> bool: response in [ self.health_check_response, # If there is a subscription - self.HEALTH_CHECK_MESSAGE, # If there is no subscriptions and decode_responses=True + self.HEALTH_CHECK_MESSAGE, # If there are no subscriptions and decode_responses=True ] ) else: