Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/8858.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stopped adding a default Content-Type header when response has no content -- by :user:`Dreamsorcerer`.
22 changes: 12 additions & 10 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, MultiDict, MultiDictProxy, MultiMapping
from yarl import URL

from . import hdrs
Expand Down Expand Up @@ -753,13 +753,15 @@ def ceil_timeout(
class HeadersMixin:
__slots__ = ("_content_type", "_content_dict", "_stored_content_type")

_headers: MultiMapping[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
Expand All @@ -774,25 +776,25 @@ 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]
assert self._content_type is not None
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]
hdrs.CONTENT_LENGTH
)
content_length = self._headers.get(hdrs.CONTENT_LENGTH)

if content_length is not None:
return int(content_length)
Expand Down
6 changes: 3 additions & 3 deletions aiohttp/web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"!#$%&'*+.^_`|~-"
Expand Down Expand Up @@ -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] = {}
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion aiohttp/web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions tests/test_client_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down
15 changes: 15 additions & 0 deletions tests/test_web_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 0 additions & 2 deletions tests/test_web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down