From c9c5e2550f7e0fca74910e46b0eba6299ee1adf7 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:07:44 +0100 Subject: [PATCH 01/11] No default Content-Type when no content --- aiohttp/helpers.py | 17 ++++++++++------- aiohttp/web_response.py | 3 ++- tests/test_web_functional.py | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 26b953ea7ce..cb03e640fa0 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -753,13 +753,15 @@ def ceil_timeout( class HeadersMixin: __slots__ = ("_content_type", "_content_dict", "_stored_content_type") + headers: CIMultiDict[str] + def __init__(self) -> None: super().__init__() self._content_type: Optional[str] = None self._content_dict: Optional[Dict[str, str]] = None - self._stored_content_type: Union[str, _SENTINEL] = sentinel + self._stored_content_type: Union[str, None, _SENTINEL] = sentinel - def _parse_content_type(self, raw: str) -> None: + def _parse_content_type(self, raw: Optional[str]) -> None: self._stored_content_type = raw if raw is None: # default value according to RFC 2616 @@ -774,23 +776,24 @@ def _parse_content_type(self, raw: str) -> None: @property def content_type(self) -> str: """The value of content part for Content-Type HTTP header.""" - raw = self._headers.get(hdrs.CONTENT_TYPE) # type: ignore[attr-defined] + 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] + return self._content_type @property def charset(self) -> Optional[str]: """The value of charset part for Content-Type HTTP header.""" - raw = self._headers.get(hdrs.CONTENT_TYPE) # type: ignore[attr-defined] + 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]: """The value of Content-Length HTTP header.""" - content_length = self._headers.get( # type: ignore[attr-defined] + content_length = self._headers.get( hdrs.CONTENT_LENGTH ) diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index 47d75675df0..e1ca8ba3fe2 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -402,7 +402,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_web_functional.py b/tests/test_web_functional.py index d82fee43623..565efc7b4ca 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -150,6 +150,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") From 8e8e876b95e6abcdbfcd09bf41c79fe47e505f3d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:09:17 +0000 Subject: [PATCH 02/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- aiohttp/helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index cb03e640fa0..374a1648edb 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -793,9 +793,7 @@ def charset(self) -> Optional[str]: @property def content_length(self) -> Optional[int]: """The value of Content-Length HTTP header.""" - content_length = self._headers.get( - hdrs.CONTENT_LENGTH - ) + content_length = self._headers.get(hdrs.CONTENT_LENGTH) if content_length is not None: return int(content_length) From 815e86fd4c4df8f3f39174d07d5c082799860a48 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:10:58 +0100 Subject: [PATCH 03/11] Create 8858.bugfix.rst --- CHANGES/8858.bugfix.rst | 1 + 1 file changed, 1 insertion(+) 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`. From 6ae390b65cf532ffdf103c9cdaf3708d5fdd2361 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:14:51 +0100 Subject: [PATCH 04/11] Update helpers.py --- aiohttp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 374a1648edb..d7bf027c74b 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -753,7 +753,7 @@ def ceil_timeout( class HeadersMixin: __slots__ = ("_content_type", "_content_dict", "_stored_content_type") - headers: CIMultiDict[str] + _headers: CIMultiDict[str] def __init__(self) -> None: super().__init__() From 49a82719542ac2d50f604b98eb3fc8342292d9ce Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:16:24 +0100 Subject: [PATCH 05/11] Update helpers.py --- aiohttp/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index d7bf027c74b..e7fbcfd7c62 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -779,6 +779,7 @@ def content_type(self) -> str: raw = self._headers.get(hdrs.CONTENT_TYPE) if self._stored_content_type != raw: self._parse_content_type(raw) + assert self._content_type is not None return self._content_type @property From f6633df162cb5d1f59b4e23bc3730e8664020a30 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:25:22 +0100 Subject: [PATCH 06/11] Update helpers.py --- aiohttp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index e7fbcfd7c62..142c50af39b 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -753,7 +753,7 @@ def ceil_timeout( class HeadersMixin: __slots__ = ("_content_type", "_content_dict", "_stored_content_type") - _headers: CIMultiDict[str] + _headers: CIMultiDictProxy[str] def __init__(self) -> None: super().__init__() From 037395d544ae1c30eee91443a6f6df1f8ea80f99 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:28:08 +0100 Subject: [PATCH 07/11] Update test_client_functional.py --- tests/test_client_functional.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 3ab798771d5..ce530eb619a 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -759,7 +759,6 @@ async def handler(request: web.Request) -> web.Response: 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), ) @@ -792,7 +791,6 @@ async def handler(request: web.Request) -> web.Response: 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), ) From 56a3ea2bf56d9d1eb4496a290b163e2e6fad077c Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:29:21 +0100 Subject: [PATCH 08/11] Update test_web_response.py --- tests/test_web_response.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_web_response.py b/tests/test_web_response.py index 74c2990cce7..f90eb4b1153 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -1006,7 +1006,6 @@ async def test_send_headers_for_empty_body(buf: Any, writer: Any) -> 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" ) @@ -1049,7 +1048,6 @@ async def test_send_set_cookie_header(buf: Any, writer: Any) -> 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" ) From 05486f5f5c71a8270391733b2cfe93a302305d95 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 13:30:40 +0100 Subject: [PATCH 09/11] Update helpers.py --- aiohttp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 142c50af39b..22e51d5644b 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -49,7 +49,7 @@ from urllib.parse import quote from urllib.request import getproxies, proxy_bypass -from multidict import CIMultiDict, MultiDict, MultiDictProxy +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy from yarl import URL from . import hdrs From f0c2c49fdc9912a935f770c90f8f6894cea89d7d Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 14:04:31 +0100 Subject: [PATCH 10/11] Update helpers.py --- aiohttp/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 22e51d5644b..2be5977c490 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -49,7 +49,7 @@ from urllib.parse import quote from urllib.request import getproxies, proxy_bypass -from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy +from multidict import CIMultiDict, MultiDict, MultiDictProxy, MultiMapping from yarl import URL from . import hdrs @@ -753,7 +753,7 @@ def ceil_timeout( class HeadersMixin: __slots__ = ("_content_type", "_content_dict", "_stored_content_type") - _headers: CIMultiDictProxy[str] + _headers: MultiMapping[str] def __init__(self) -> None: super().__init__() From 4d8bafb0a76b9166e3d80db0fe7df31fecfb22c2 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 23 Aug 2024 14:53:37 +0100 Subject: [PATCH 11/11] Fix --- aiohttp/web_request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 828776d82e5..4dbc19a4849 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -82,7 +82,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"!#$%&'*+.^_`|~-" @@ -171,7 +171,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] = {} @@ -483,7 +483,7 @@ def query_string(self) -> str: return self._rel_url.query_string @reify - def headers(self) -> "CIMultiDictProxy[str]": + def headers(self) -> CIMultiDictProxy[str]: """A case-insensitive multidict proxy with all headers.""" return self._headers