diff --git a/CHANGES/13099.bugfix.rst b/CHANGES/13099.bugfix.rst new file mode 100644 index 00000000000..3fc027b022a --- /dev/null +++ b/CHANGES/13099.bugfix.rst @@ -0,0 +1 @@ +Fixed ``BaseRequest.text()`` and ``application/x-www-form-urlencoded`` request parsing to return HTTP 415 when body bytes fail to decode with the default charset -- by :user:`cyphercodes`. diff --git a/README.rst b/README.rst index dfd14ae7c0d..0f8790be1f8 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,7 @@ To get something from the web: async def main(): async with aiohttp.ClientSession() as session: - async with session.get('http://python.org') as response: + async with session.get('https://python.org') as response: print("Status:", response.status) print("Content-type:", response.headers['content-type']) @@ -134,11 +134,11 @@ External links ============== * `Third party libraries - `_ + `_ * `Built with aiohttp - `_ + `_ * `Powered by aiohttp - `_ + `_ Feel free to make a Pull Request for adding your link to these pages! diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 642f4e47387..5d152dedcd2 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -665,7 +665,7 @@ async def text(self) -> str: encoding = self.charset or "utf-8" try: return bytes_body.decode(encoding) - except LookupError: + except (LookupError, UnicodeDecodeError): raise HTTPUnsupportedMediaType() async def json( @@ -790,7 +790,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]": bytes_query = data.rstrip() try: query = bytes_query.decode(charset) - except LookupError: + except (LookupError, UnicodeDecodeError): raise HTTPUnsupportedMediaType() out.extend( parse_qsl(qs=query, keep_blank_values=True, encoding=charset) diff --git a/tests/test_web_request.py b/tests/test_web_request.py index 9dd919caf09..b518d2a513f 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -930,6 +930,22 @@ async def test_request_with_wrong_content_type_encoding(protocol: BaseProtocol) assert err.value.status_code == 415 +async def test_request_text_with_invalid_default_encoding( + protocol: BaseProtocol, +) -> None: + payload = StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) + payload.feed_data(b"\xff") + payload.feed_eof() + headers = {"Content-Type": "text/html"} + req = make_mocked_request("POST", "/", payload=payload, headers=headers) + + with pytest.raises(web.HTTPUnsupportedMediaType) as err: + await req.text() + assert err.value.status_code == 415 + + async def test_make_too_big_request_same_size_to_max(protocol: BaseProtocol) -> None: payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) large_file = 1024**2 * b"x" @@ -977,6 +993,24 @@ async def test_multipart_formdata(protocol: BaseProtocol) -> None: assert dict(result) == {"a": "b", "c": "d"} +async def test_urlencoded_form_with_invalid_default_encoding( + protocol: BaseProtocol, +) -> None: + payload = StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) + payload.feed_data(b"a=1&b=\xff") + payload.feed_eof() + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + req = make_mocked_request("POST", "/", payload=payload, headers=headers) + + with pytest.raises(web.HTTPUnsupportedMediaType) as err: + await req.post() + assert err.value.status_code == 415 + + async def test_multipart_formdata_field_missing_name(protocol: BaseProtocol) -> None: # Ensure ValueError is raised when Content-Disposition has no name payload = StreamReader(