From 6e306f461f0a340fdb31dc2266592203f6d8f557 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 16:13:54 +0100 Subject: [PATCH] No default Content-Type when no content (#8858) (cherry picked from commit 26772ad320c1ee0efa5b91ae35ae5860a37cd709) --- CHANGES/8858.bugfix.rst | 1 + aiohttp/helpers.py | 6 ++++-- aiohttp/web_request.py | 6 +++--- aiohttp/web_response.py | 3 ++- tests/test_client_functional.py | 2 -- tests/test_web_functional.py | 15 +++++++++++++++ tests/test_web_response.py | 2 -- 7 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 CHANGES/8858.bugfix.rst diff --git a/CHANGES/8858.bugfix.rst b/CHANGES/8858.bugfix.rst new file mode 100644 index 00000000000..e4efa91a2fd --- /dev/null +++ b/CHANGES/8858.bugfix.rst @@ -0,0 +1 @@ +Stopped adding a default Content-Type header when response has no content -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index ccfa9d5e2fe..f759bddc099 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -762,7 +762,8 @@ def content_type(self) -> str: raw = self._headers.get(hdrs.CONTENT_TYPE) if self._stored_content_type != raw: self._parse_content_type(raw) - return self._content_type # type: ignore[return-value] + assert self._content_type is not None + return self._content_type @property def charset(self) -> Optional[str]: @@ -770,7 +771,8 @@ def charset(self) -> Optional[str]: raw = self._headers.get(hdrs.CONTENT_TYPE) if self._stored_content_type != raw: self._parse_content_type(raw) - return self._content_dict.get("charset") # type: ignore[union-attr] + assert self._content_dict is not None + return self._content_dict.get("charset") @property def content_length(self) -> Optional[int]: diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index a485f0dcea6..a63d3074ea5 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -79,7 +79,7 @@ class FileField: filename: str file: io.BufferedReader content_type: str - headers: "CIMultiDictProxy[str]" + headers: CIMultiDictProxy[str] _TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" @@ -169,7 +169,7 @@ def __init__( self._payload_writer = payload_writer self._payload = payload - self._headers = message.headers + self._headers: CIMultiDictProxy[str] = message.headers self._method = message.method self._version = message.version self._cache: Dict[str, Any] = {} @@ -493,7 +493,7 @@ def query_string(self) -> str: return self._rel_url.query_string @reify - def headers(self) -> "MultiMapping[str]": + def headers(self) -> CIMultiDictProxy[str]: """A case-insensitive multidict proxy with all headers.""" return self._headers diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index 78d3fe32949..f2a769e1bdd 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -479,7 +479,8 @@ async def _prepare_headers(self) -> None: # https://datatracker.ietf.org/doc/html/rfc9112#section-6.1-13 if hdrs.TRANSFER_ENCODING in headers: del headers[hdrs.TRANSFER_ENCODING] - else: + elif self.content_length != 0: + # https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5 headers.setdefault(hdrs.CONTENT_TYPE, "application/octet-stream") headers.setdefault(hdrs.DATE, rfc822_formatted_time()) headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 872876d4a32..566c47522ce 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -727,7 +727,6 @@ async def handler(request): raw_headers = tuple((bytes(h), bytes(v)) for h, v in resp.raw_headers) assert raw_headers == ( (b"Content-Length", b"0"), - (b"Content-Type", b"application/octet-stream"), (b"Date", mock.ANY), (b"Server", mock.ANY), ) @@ -760,7 +759,6 @@ async def handler(request): assert raw_headers == ( (b"X-Empty", b""), (b"Content-Length", b"0"), - (b"Content-Type", b"application/octet-stream"), (b"Date", mock.ANY), (b"Server", mock.ANY), ) diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index ee61537068b..96a4f82ba9f 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -148,6 +148,21 @@ async def handler(request): assert resp.headers["Content-Length"] == "4" +@pytest.mark.parametrize("status", (201, 204, 404)) +async def test_default_content_type_no_body(aiohttp_client: Any, status: int) -> None: + async def handler(request): + return web.Response(status=status) + + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + + async with client.get("/") as resp: + assert resp.status == status + assert await resp.read() == b"" + assert "Content-Type" not in resp.headers + + async def test_response_before_complete(aiohttp_client: Any) -> None: async def handler(request): return web.Response(body=b"OK") diff --git a/tests/test_web_response.py b/tests/test_web_response.py index d1b407c090c..ad1286ca91e 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -1139,7 +1139,6 @@ async def test_send_headers_for_empty_body(buf, writer) -> None: Matches( "HTTP/1.1 200 OK\r\n" "Content-Length: 0\r\n" - "Content-Type: application/octet-stream\r\n" "Date: .+\r\n" "Server: .+\r\n\r\n" ) @@ -1182,7 +1181,6 @@ async def test_send_set_cookie_header(buf, writer) -> None: "HTTP/1.1 200 OK\r\n" "Content-Length: 0\r\n" "Set-Cookie: name=value\r\n" - "Content-Type: application/octet-stream\r\n" "Date: .+\r\n" "Server: .+\r\n\r\n" )