Skip to content

Commit 9770b39

Browse files
committed
Ensure that application/octet-stream is the default content_type (aio-libs#11580)
(cherry picked from commit d261f8a)
1 parent 325b680 commit 9770b39

6 files changed

Lines changed: 68 additions & 12 deletions

File tree

CHANGES/10889.bugfix.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Updated ``Content-Type`` header parsing to return ``application/octet-stream`` when header contains invalid syntax.
2+
See :rfc:`9110#section-8.3-5`.
3+
4+
-- by :user:`sgaist`.

CONTRIBUTORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ Roman Postnov
311311
Rong Zhang
312312
Samir Akarioh
313313
Samuel Colvin
314+
Samuel Gaist
314315
Sean Hunt
315316
Sebastian Acuna
316317
Sebastian Hanula

aiohttp/helpers.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from collections import namedtuple
1919
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping
2020
from contextlib import suppress
21+
from email.message import EmailMessage
2122
from email.parser import HeaderParser
23+
from email.policy import HTTP
2224
from email.utils import parsedate
2325
from math import ceil
2426
from pathlib import Path
@@ -347,14 +349,40 @@ def parse_mimetype(mimetype: str) -> MimeType:
347349
)
348350

349351

352+
class EnsureOctetStream(EmailMessage):
353+
def __init__(self) -> None:
354+
super().__init__()
355+
# https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5
356+
self.set_default_type("application/octet-stream")
357+
358+
def get_content_type(self) -> Any:
359+
"""Re-implementation from Message
360+
361+
Returns application/octet-stream in place of plain/text when
362+
value is wrong.
363+
364+
The way this class is used guarantees that content-type will
365+
be present so simplify the checks wrt to the base implementation.
366+
"""
367+
value = self.get("content-type", "").lower()
368+
369+
# Based on the implementation of _splitparam in the standard library
370+
ctype, _, _ = value.partition(";")
371+
ctype = ctype.strip()
372+
if ctype.count("/") != 1:
373+
return self.get_default_type()
374+
return ctype
375+
376+
350377
@functools.lru_cache(maxsize=56)
351378
def parse_content_type(raw: str) -> tuple[str, MappingProxyType[str, str]]:
352379
"""Parse Content-Type header.
353380
354381
Returns a tuple of the parsed content type and a
355-
MappingProxyType of parameters.
382+
MappingProxyType of parameters. The default returned value
383+
is `application/octet-stream`
356384
"""
357-
msg = HeaderParser().parsestr(f"Content-Type: {raw}")
385+
msg = HeaderParser(EnsureOctetStream, policy=HTTP).parsestr(f"Content-Type: {raw}")
358386
content_type = msg.get_content_type()
359387
params = msg.get_params(())
360388
content_dict = dict(params[1:]) # First element is content type again

docs/client_reference.rst

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,16 +1566,14 @@ Response object
15661566

15671567
.. note::
15681568

1569-
Returns value is ``'application/octet-stream'`` if no
1570-
Content-Type header present in HTTP headers according to
1571-
:rfc:`9110`. If the *Content-Type* header is invalid (e.g., ``jpg``
1572-
instead of ``image/jpeg``), the value is ``text/plain`` by default
1573-
according to :rfc:`2045`. To see the original header check
1574-
``resp.headers['CONTENT-TYPE']``.
1569+
Returns ``'application/octet-stream'`` if no Content-Type header
1570+
is present or the value contains invalid syntax according to
1571+
:rfc:`9110`. To see the original header check
1572+
``resp.headers["Content-Type"]``.
15751573

15761574
To make sure Content-Type header is not present in
15771575
the server reply, use :attr:`headers` or :attr:`raw_headers`, e.g.
1578-
``'CONTENT-TYPE' not in resp.headers``.
1576+
``'Content-Type' not in resp.headers``.
15791577

15801578
.. attribute:: charset
15811579

tests/test_helpers.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import weakref
77
from math import ceil, modf
88
from pathlib import Path
9+
from types import MappingProxyType
910
from unittest import mock
1011
from urllib.request import getproxies_environment
1112

1213
import pytest
13-
from multidict import MultiDict
14+
from multidict import MultiDict, MultiDictProxy
1415
from yarl import URL
1516

1617
from aiohttp import helpers
@@ -65,6 +66,30 @@ def test_parse_mimetype(mimetype, expected) -> None:
6566
assert result == expected
6667

6768

69+
# ------------------- parse_content_type ------------------------------
70+
71+
72+
@pytest.mark.parametrize(
73+
"content_type, expected",
74+
[
75+
(
76+
"text/plain",
77+
("text/plain", MultiDictProxy(MultiDict())),
78+
),
79+
(
80+
"wrong",
81+
("application/octet-stream", MultiDictProxy(MultiDict())),
82+
),
83+
],
84+
)
85+
def test_parse_content_type(
86+
content_type: str, expected: tuple[str, MappingProxyType[str, str]]
87+
) -> None:
88+
result = helpers.parse_content_type(content_type)
89+
90+
assert result == expected
91+
92+
6893
# ------------------- guess_filename ----------------------------------
6994

7095

tests/test_web_response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,10 +1164,10 @@ def test_ctor_content_type_with_extra() -> None:
11641164
assert resp.headers["content-type"] == "text/plain; version=0.0.4; charset=utf-8"
11651165

11661166

1167-
def test_invalid_content_type_parses_to_text_plain() -> None:
1167+
def test_invalid_content_type_parses_to_application_octect_stream() -> None:
11681168
resp = Response(text="test test", content_type="jpeg")
11691169

1170-
assert resp.content_type == "text/plain"
1170+
assert resp.content_type == "application/octet-stream"
11711171
assert resp.headers["content-type"] == "jpeg; charset=utf-8"
11721172

11731173

0 commit comments

Comments
 (0)