From f9d1cbbbd1169bab884602e02d53d0e527b8142e Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Thu, 15 Oct 2026 04:15:31 +0200 Subject: [PATCH 1/2] fix: read rate limit headers case-insensitively The async client passes response headers as a plain dict, which keeps the case the server sent, so rate_limit() returned None when the server sent X-Ratelimit-* instead of x-ratelimit-*. Also poll in test_channel_hide_show: the backend unhides a channel in a background task after a new message. Co-Authored-By: Claude Opus 5.5 --- stream_chat/tests/async_chat/test_channel.py | 14 +++++++---- stream_chat/tests/test_channel.py | 13 +++++++---- .../tests/test_stream_response_headers.py | 23 +++++++++++++++++++ stream_chat/types/stream_response.py | 9 +++++--- 4 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 stream_chat/tests/test_stream_response_headers.py diff --git a/stream_chat/tests/async_chat/test_channel.py b/stream_chat/tests/async_chat/test_channel.py index d0d6447d..23209a24 100644 --- a/stream_chat/tests/async_chat/test_channel.py +++ b/stream_chat/tests/async_chat/test_channel.py @@ -1,3 +1,4 @@ +import asyncio import time import uuid from pathlib import Path @@ -300,10 +301,15 @@ async def test_channel_hide_show( assert len(response["channels"]) == 0 # send message await channel.send_message({"text": "hi"}, random_users[1]["id"]) - # channel should be listed now - response = await client.query_channels( - {"id": channel.id}, user_id=random_users[0]["id"] - ) + # channel should be listed now. The backend unhides it in a background + # task after the message is sent, so poll for it. + for _ in range(20): + response = await client.query_channels( + {"id": channel.id}, user_id=random_users[0]["id"] + ) + if len(response["channels"]) == 1: + break + await asyncio.sleep(0.5) assert len(response["channels"]) == 1 async def test_invites(self, client: StreamChatAsync, random_users: List[Dict]): diff --git a/stream_chat/tests/test_channel.py b/stream_chat/tests/test_channel.py index 193b44be..79105af0 100644 --- a/stream_chat/tests/test_channel.py +++ b/stream_chat/tests/test_channel.py @@ -302,10 +302,15 @@ def test_channel_hide_show( assert len(response["channels"]) == 0 # send message channel.send_message({"text": "hi"}, random_users[1]["id"]) - # channel should be listed now - response = client.query_channels( - {"id": channel.id}, user_id=random_users[0]["id"] - ) + # channel should be listed now. The backend unhides it in a background + # task after the message is sent, so poll for it. + for _ in range(20): + response = client.query_channels( + {"id": channel.id}, user_id=random_users[0]["id"] + ) + if len(response["channels"]) == 1: + break + time.sleep(0.5) assert len(response["channels"]) == 1 def test_invites(self, client: StreamChat, random_users: List[Dict]): diff --git a/stream_chat/tests/test_stream_response_headers.py b/stream_chat/tests/test_stream_response_headers.py new file mode 100644 index 00000000..830e74a4 --- /dev/null +++ b/stream_chat/tests/test_stream_response_headers.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from stream_chat.types.stream_response import StreamResponse + + +class TestStreamResponseHeaders: + def test_rate_limit_reads_headers_in_any_case(self): + resp = StreamResponse( + {"duration": "1ms"}, + { + "X-Ratelimit-Limit": "1000", + "X-Ratelimit-Remaining": "999", + "X-Ratelimit-Reset": "1700000000", + }, + 200, + ) + + rate_limit = resp.rate_limit() + assert rate_limit is not None + assert rate_limit.limit == 1000 + assert rate_limit.remaining == 999 + assert type(rate_limit.reset) is datetime + assert resp.headers()["X-Ratelimit-Limit"] == "1000" diff --git a/stream_chat/types/stream_response.py b/stream_chat/types/stream_response.py index b42ac88c..21d95b44 100644 --- a/stream_chat/types/stream_response.py +++ b/stream_chat/types/stream_response.py @@ -31,10 +31,13 @@ def __init__( self.__headers = headers self.__status_code = status_code self.__rate_limit: Optional[RateLimitInfo] = None + # Header names are case-insensitive. The async client passes a plain + # dict, which keeps the case the server sent (e.g. X-Ratelimit-Limit). + lowered = {k.lower(): v for k, v in headers.items()} limit, remaining, reset = ( - headers.get("x-ratelimit-limit"), - headers.get("x-ratelimit-remaining"), - headers.get("x-ratelimit-reset"), + lowered.get("x-ratelimit-limit"), + lowered.get("x-ratelimit-remaining"), + lowered.get("x-ratelimit-reset"), ) if limit and remaining and reset: self.__rate_limit = RateLimitInfo( From 39f7537d19b556b302442575f8b1f235013fd32f Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Thu, 15 Oct 2026 05:04:27 +0200 Subject: [PATCH 2/2] test: poll up to 30s for the unhide in hide/show tests Co-Authored-By: Claude Opus 5.5 --- stream_chat/tests/async_chat/test_channel.py | 5 +++-- stream_chat/tests/test_channel.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/stream_chat/tests/async_chat/test_channel.py b/stream_chat/tests/async_chat/test_channel.py index 23209a24..dc2bf0eb 100644 --- a/stream_chat/tests/async_chat/test_channel.py +++ b/stream_chat/tests/async_chat/test_channel.py @@ -302,8 +302,9 @@ async def test_channel_hide_show( # send message await channel.send_message({"text": "hi"}, random_users[1]["id"]) # channel should be listed now. The backend unhides it in a background - # task after the message is sent, so poll for it. - for _ in range(20): + # task after the message is sent, which can take a while on a busy + # shared test app, so poll for up to 30s. + for _ in range(60): response = await client.query_channels( {"id": channel.id}, user_id=random_users[0]["id"] ) diff --git a/stream_chat/tests/test_channel.py b/stream_chat/tests/test_channel.py index 79105af0..a61c4f4f 100644 --- a/stream_chat/tests/test_channel.py +++ b/stream_chat/tests/test_channel.py @@ -303,8 +303,9 @@ def test_channel_hide_show( # send message channel.send_message({"text": "hi"}, random_users[1]["id"]) # channel should be listed now. The backend unhides it in a background - # task after the message is sent, so poll for it. - for _ in range(20): + # task after the message is sent, which can take a while on a busy + # shared test app, so poll for up to 30s. + for _ in range(60): response = client.query_channels( {"id": channel.id}, user_id=random_users[0]["id"] )