Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No default Content-Type when no content #8858

Merged
merged 11 commits into from
Aug 23, 2024
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
Loading