From 89951ecf658a49cfe477d1a389336f90a043ff2d Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 19:04:02 +0100 Subject: [PATCH] [PR #9110/b90dd1e4 backport][3.10] Avoid compressing empty body (#9108) (#9111) **This is a backport of PR #9110 as merged into 3.11 (b90dd1e41f77e3296edf8932cc0ec59ab1343463).** (cherry picked from commit 1d112418a05dcdcabd38590351e78bec8f4a45bc) Co-authored-by: Sam Bull --- CHANGES/9108.bugfix.rst | 1 + aiohttp/client.py | 4 +-- aiohttp/client_reqrep.py | 8 +++--- tests/test_client_functional.py | 45 ++++++++++++++++++++++++++++++--- 4 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 CHANGES/9108.bugfix.rst diff --git a/CHANGES/9108.bugfix.rst b/CHANGES/9108.bugfix.rst new file mode 100644 index 00000000000..8be000575e8 --- /dev/null +++ b/CHANGES/9108.bugfix.rst @@ -0,0 +1 @@ +Fixed compressed requests failing when no body was provided -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 2814edc31ee..5f9e95f4706 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -165,7 +165,7 @@ class _RequestOptions(TypedDict, total=False): auth: Union[BasicAuth, None] allow_redirects: bool max_redirects: int - compress: Union[str, None] + compress: Union[str, bool, None] chunked: Union[bool, None] expect100: bool raise_for_status: Union[None, bool, Callable[[ClientResponse], Awaitable[None]]] @@ -459,7 +459,7 @@ async def _request( auth: Optional[BasicAuth] = None, allow_redirects: bool = True, max_redirects: int = 10, - compress: Optional[str] = None, + compress: Union[str, bool, None] = None, chunked: Optional[bool] = None, expect100: bool = False, raise_for_status: Union[ diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 2df43d112cd..93e7b59a8a1 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -272,7 +272,7 @@ def __init__( cookies: Optional[LooseCookies] = None, auth: Optional[BasicAuth] = None, version: http.HttpVersion = http.HttpVersion11, - compress: Optional[str] = None, + compress: Union[str, bool, None] = None, chunked: Optional[bool] = None, expect100: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -503,7 +503,9 @@ def update_cookies(self, cookies: Optional[LooseCookies]) -> None: def update_content_encoding(self, data: Any) -> None: """Set request content encoding.""" - if data is None: + if not data: + # Don't compress an empty body. + self.compress = None return enc = self.headers.get(hdrs.CONTENT_ENCODING, "").lower() @@ -714,7 +716,7 @@ async def send(self, conn: "Connection") -> "ClientResponse": ) if self.compress: - writer.enable_compression(self.compress) + writer.enable_compression(self.compress) # type: ignore[arg-type] if self.chunked is not None: writer.enable_chunking() diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 9325cc17e48..082db6f3e9a 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -12,7 +12,7 @@ import tarfile import time import zipfile -from typing import Any, AsyncIterator, Type +from typing import Any, AsyncIterator, Optional, Type from unittest import mock import pytest @@ -31,6 +31,9 @@ SocketTimeoutError, TooManyRedirects, ) +from aiohttp.client_reqrep import ClientRequest +from aiohttp.connector import Connection +from aiohttp.http_writer import StreamWriter from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer, TestClient from aiohttp.test_utils import unused_port @@ -1510,8 +1513,44 @@ async def handler(request): assert 200 == resp.status -async def test_POST_DATA_DEFLATE(aiohttp_client) -> None: - async def handler(request): +@pytest.mark.parametrize("data", (None, b"")) +async def test_GET_DEFLATE( + aiohttp_client: AiohttpClient, data: Optional[bytes] +) -> None: + async def handler(request: web.Request) -> web.Response: + return web.json_response({"ok": True}) + + write_mock = None + original_write_bytes = ClientRequest.write_bytes + + async def write_bytes( + self: ClientRequest, writer: StreamWriter, conn: Connection + ) -> None: + nonlocal write_mock + original_write = writer._write + + with mock.patch.object( + writer, "_write", autospec=True, spec_set=True, side_effect=original_write + ) as write_mock: + await original_write_bytes(self, writer, conn) + + with mock.patch.object(ClientRequest, "write_bytes", write_bytes): + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + + async with client.get("/", data=data, compress=True) as resp: + assert resp.status == 200 + content = await resp.json() + assert content == {"ok": True} + + assert write_mock is not None + # No chunks should have been sent for an empty body. + write_mock.assert_not_called() + + +async def test_POST_DATA_DEFLATE(aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> web.Response: data = await request.post() return web.json_response(dict(data))