Skip to content

Commit 24c93b2

Browse files
[CHA-0] fix: read rate limit headers case-insensitively; poll in hide/show test (#243)
* 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 <noreply@anthropic.com> * test: poll up to 30s for the unhide in hide/show tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c45906d commit 24c93b2

4 files changed

Lines changed: 50 additions & 11 deletions

File tree

‎stream_chat/tests/async_chat/test_channel.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import time
23
import uuid
34
from pathlib import Path
@@ -300,10 +301,16 @@ async def test_channel_hide_show(
300301
assert len(response["channels"]) == 0
301302
# send message
302303
await channel.send_message({"text": "hi"}, random_users[1]["id"])
303-
# channel should be listed now
304-
response = await client.query_channels(
305-
{"id": channel.id}, user_id=random_users[0]["id"]
306-
)
304+
# channel should be listed now. The backend unhides it in a background
305+
# task after the message is sent, which can take a while on a busy
306+
# shared test app, so poll for up to 30s.
307+
for _ in range(60):
308+
response = await client.query_channels(
309+
{"id": channel.id}, user_id=random_users[0]["id"]
310+
)
311+
if len(response["channels"]) == 1:
312+
break
313+
await asyncio.sleep(0.5)
307314
assert len(response["channels"]) == 1
308315

309316
async def test_invites(self, client: StreamChatAsync, random_users: List[Dict]):

‎stream_chat/tests/test_channel.py‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,16 @@ def test_channel_hide_show(
302302
assert len(response["channels"]) == 0
303303
# send message
304304
channel.send_message({"text": "hi"}, random_users[1]["id"])
305-
# channel should be listed now
306-
response = client.query_channels(
307-
{"id": channel.id}, user_id=random_users[0]["id"]
308-
)
305+
# channel should be listed now. The backend unhides it in a background
306+
# task after the message is sent, which can take a while on a busy
307+
# shared test app, so poll for up to 30s.
308+
for _ in range(60):
309+
response = client.query_channels(
310+
{"id": channel.id}, user_id=random_users[0]["id"]
311+
)
312+
if len(response["channels"]) == 1:
313+
break
314+
time.sleep(0.5)
309315
assert len(response["channels"]) == 1
310316

311317
def test_invites(self, client: StreamChat, random_users: List[Dict]):
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from datetime import datetime
2+
3+
from stream_chat.types.stream_response import StreamResponse
4+
5+
6+
class TestStreamResponseHeaders:
7+
def test_rate_limit_reads_headers_in_any_case(self):
8+
resp = StreamResponse(
9+
{"duration": "1ms"},
10+
{
11+
"X-Ratelimit-Limit": "1000",
12+
"X-Ratelimit-Remaining": "999",
13+
"X-Ratelimit-Reset": "1700000000",
14+
},
15+
200,
16+
)
17+
18+
rate_limit = resp.rate_limit()
19+
assert rate_limit is not None
20+
assert rate_limit.limit == 1000
21+
assert rate_limit.remaining == 999
22+
assert type(rate_limit.reset) is datetime
23+
assert resp.headers()["X-Ratelimit-Limit"] == "1000"

‎stream_chat/types/stream_response.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ def __init__(
3131
self.__headers = headers
3232
self.__status_code = status_code
3333
self.__rate_limit: Optional[RateLimitInfo] = None
34+
# Header names are case-insensitive. The async client passes a plain
35+
# dict, which keeps the case the server sent (e.g. X-Ratelimit-Limit).
36+
lowered = {k.lower(): v for k, v in headers.items()}
3437
limit, remaining, reset = (
35-
headers.get("x-ratelimit-limit"),
36-
headers.get("x-ratelimit-remaining"),
37-
headers.get("x-ratelimit-reset"),
38+
lowered.get("x-ratelimit-limit"),
39+
lowered.get("x-ratelimit-remaining"),
40+
lowered.get("x-ratelimit-reset"),
3841
)
3942
if limit and remaining and reset:
4043
self.__rate_limit = RateLimitInfo(

0 commit comments

Comments
 (0)