From 1078f6d4a81c5ab5561ced6abb3cc61f1114dcc0 Mon Sep 17 00:00:00 2001 From: KeepingRunning <1599949878@qq.com> Date: Thu, 26 Mar 2026 14:12:37 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Daiohttp=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=93=8D=E5=BA=94=E5=88=86=E5=9D=97=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E4=B8=8D=E5=9B=BA=E5=AE=9A=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot/drivers/aiohttp.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/nonebot/drivers/aiohttp.py b/nonebot/drivers/aiohttp.py index e2140217a464..cb9aa810cd79 100644 --- a/nonebot/drivers/aiohttp.py +++ b/nonebot/drivers/aiohttp.py @@ -191,11 +191,26 @@ async def stream_request( timeout=timeout, ) as response: response_headers = response.headers.copy() + # aiohttp does not guarantee fixed-size chunks; re-chunk to exact size + buffer = bytearray() async for chunk in response.content.iter_chunked(chunk_size): + if not chunk: + continue + buffer.extend(chunk) + while len(buffer) >= chunk_size: + out = bytes(buffer[:chunk_size]) + del buffer[:chunk_size] + yield Response( + response.status, + headers=response_headers, + content=out, + request=setup, + ) + if buffer: yield Response( response.status, headers=response_headers, - content=chunk, + content=bytes(buffer), request=setup, ) From f3a2667c648ea1edf6266029ea4c2e9bd235f644 Mon Sep 17 00:00:00 2001 From: KeepingRunning <1599949878@qq.com> Date: Thu, 26 Mar 2026 21:44:36 +0800 Subject: [PATCH 2/4] =?UTF-8?q?Fix:=20=E8=A1=A5=E5=85=A8=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_driver.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_driver.py b/tests/test_driver.py index d333166cd9fc..b327f9bfb328 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -22,6 +22,7 @@ WebSocketClientMixin, WebSocketServerSetup, ) +from nonebot.drivers.aiohttp import Session as AiohttpSession from nonebot.drivers.aiohttp import WebSocket as AiohttpWebSocket from nonebot.exception import WebSocketClosed from nonebot.params import Depends @@ -596,6 +597,43 @@ async def test_http_client_session(driver: Driver, server_url: URL): await anyio.sleep(1) +@pytest.mark.anyio +async def test_aiohttp_stream_request_skip_empty_chunk() -> None: + class _FakeContent: + async def iter_chunked(self, _: int): + for chunk in (b"ab", b"", b"cd", b"e"): + yield chunk + + class _FakeResponse: + status = 200 + headers = {"x-test": "1"} + content = _FakeContent() + + class _FakeRequestContext: + async def __aenter__(self) -> _FakeResponse: + return _FakeResponse() + + async def __aexit__(self, *args: object) -> bool: + return False + + class _FakeClient: + def request(self, *args: object, **kwargs: object) -> _FakeRequestContext: + return _FakeRequestContext() + + session = AiohttpSession() + session._client = _FakeClient() # type: ignore[assignment] + + chunks = [] + async for resp in session.stream_request(Request("GET", "https://example.com"), chunk_size=2): + assert resp.status_code == 200 + assert resp.content + chunks.append(resp.content) + + assert chunks == [b"ab", b"cd", b"e"] + assert b"".join(chunks) == b"abcde" + assert all(len(chunk) == 2 for chunk in chunks[:-1]) + + @pytest.mark.anyio @pytest.mark.parametrize( "driver", From f7593d210e4ac9bc2b7f9413f635f651f56fda42 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:45:26 +0000 Subject: [PATCH 3/4] :rotating_light: auto fix by pre-commit hooks --- tests/test_driver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_driver.py b/tests/test_driver.py index b327f9bfb328..5695fd6add4e 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -624,7 +624,9 @@ def request(self, *args: object, **kwargs: object) -> _FakeRequestContext: session._client = _FakeClient() # type: ignore[assignment] chunks = [] - async for resp in session.stream_request(Request("GET", "https://example.com"), chunk_size=2): + async for resp in session.stream_request( + Request("GET", "https://example.com"), chunk_size=2 + ): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) From 1df3a1b286f0de06560b1e0240aef942b8b541ab Mon Sep 17 00:00:00 2001 From: KeepingRunning <1599949878@qq.com> Date: Thu, 26 Mar 2026 21:47:12 +0800 Subject: [PATCH 4/4] =?UTF-8?q?Fix:=20=E4=BF=AE=E5=A4=8D=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E9=80=9A=E8=BF=87ruff=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_driver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_driver.py b/tests/test_driver.py index 5695fd6add4e..1b7a2a33b9d7 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -605,9 +605,10 @@ async def iter_chunked(self, _: int): yield chunk class _FakeResponse: - status = 200 - headers = {"x-test": "1"} - content = _FakeContent() + def __init__(self) -> None: + self.status = 200 + self.headers = {"x-test": "1"} + self.content = _FakeContent() class _FakeRequestContext: async def __aenter__(self) -> _FakeResponse: