From 570cfe87046ddd52ddf9232cb0245d157ba3664a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= <25355197+provinzkraut@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:31:58 +0200 Subject: [PATCH] fix: don't implicitly parse form data as JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> --- litestar/_parsers.py | 24 +++++++++++++++++------- poetry.lock | 8 ++++---- pyproject.toml | 7 ++++--- tests/unit/test_parsers.py | 8 ++++---- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/litestar/_parsers.py b/litestar/_parsers.py index 5d66636d73..49b795514d 100644 --- a/litestar/_parsers.py +++ b/litestar/_parsers.py @@ -1,18 +1,25 @@ from __future__ import annotations +from collections import defaultdict from functools import lru_cache from http.cookies import _unquote as unquote_cookie -from typing import Any, Iterable +from typing import Iterable from urllib.parse import unquote -from fast_query_parsers import parse_query_string as fast_parse_query_string -from fast_query_parsers import parse_url_encoded_dict +try: + from fast_query_parsers import parse_query_string as parse_qsl +except ImportError: + from urllib.parse import parse_qsl as _parse_qsl + + def parse_qsl(qs: bytes, separator: str) -> list[tuple[str, str]]: + return _parse_qsl(qs.decode("latin-1"), keep_blank_values=True, separator=separator) + __all__ = ("parse_cookie_string", "parse_headers", "parse_query_string", "parse_url_encoded_form_data") @lru_cache(1024) -def parse_url_encoded_form_data(encoded_data: bytes) -> dict[str, Any]: +def parse_url_encoded_form_data(encoded_data: bytes) -> dict[str, str | list[str]]: """Parse an url encoded form data dict. Args: @@ -21,11 +28,14 @@ def parse_url_encoded_form_data(encoded_data: bytes) -> dict[str, Any]: Returns: A parsed dict. """ - return parse_url_encoded_dict(qs=encoded_data, parse_numbers=False) + decoded_dict: defaultdict[str, list[str]] = defaultdict(list) + for k, v in parse_qsl(encoded_data, separator="&"): + decoded_dict[k].append(v) + return {k: v if len(v) > 1 else v[0] for k, v in decoded_dict.items()} @lru_cache(1024) -def parse_query_string(query_string: bytes) -> tuple[tuple[str, Any], ...]: +def parse_query_string(query_string: bytes) -> tuple[tuple[str, str], ...]: """Parse a query string into a tuple of key value pairs. Args: @@ -34,7 +44,7 @@ def parse_query_string(query_string: bytes) -> tuple[tuple[str, Any], ...]: Returns: A tuple of key value pairs. """ - return tuple(fast_parse_query_string(query_string, "&")) + return tuple(parse_qsl(query_string, separator="&")) @lru_cache(1024) diff --git a/poetry.lock b/poetry.lock index edb008e6ad..b179618e72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1104,7 +1104,7 @@ typing-extensions = {version = ">=3.10.0.1", markers = "python_version <= \"3.8\ name = "fast-query-parsers" version = "1.0.3" description = "Ultra-fast query string and url-encoded form-data parsers" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "fast_query_parsers-1.0.3-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:afbf71c1b4398dacfb9d84755eb026f8e759f68a066f1f3cc19e471fc342e74f"}, @@ -4293,7 +4293,7 @@ attrs = ["attrs"] brotli = ["brotli"] cli = ["jsbeautifier", "uvicorn"] cryptography = ["cryptography"] -full = ["advanced-alchemy", "annotated-types", "attrs", "brotli", "cryptography", "jinja2", "jsbeautifier", "mako", "minijinja", "opentelemetry-instrumentation-asgi", "piccolo", "picologging", "prometheus-client", "pydantic", "pydantic-extra-types", "python-jose", "redis", "structlog", "uvicorn"] +full = ["advanced-alchemy", "annotated-types", "attrs", "brotli", "cryptography", "fast-query-parsers", "jinja2", "jsbeautifier", "mako", "minijinja", "opentelemetry-instrumentation-asgi", "piccolo", "picologging", "prometheus-client", "pydantic", "pydantic-extra-types", "python-jose", "redis", "structlog", "uvicorn"] jinja = ["jinja2"] jsbeautifier = ["jsbeautifier"] jwt = ["cryptography", "python-jose"] @@ -4306,10 +4306,10 @@ prometheus = ["prometheus-client"] pydantic = ["pydantic", "pydantic-extra-types"] redis = ["redis"] sqlalchemy = ["advanced-alchemy"] -standard = ["jinja2", "jsbeautifier", "uvicorn"] +standard = ["fast-query-parsers", "jinja2", "jsbeautifier", "uvicorn"] structlog = ["structlog"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "ea7b1c0ac6f00e05451ed86ad33c3d47bb46418d728beb69780f42c61f850f3f" +content-hash = "162ff5be14a8e045a386af07cecc80464c936d38bd59f04d98f4bdb2d45338bd" diff --git a/pyproject.toml b/pyproject.toml index a16f1d7f2a..6e4a6e4c84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ attrs = { version = "*", optional = true } brotli = { version = "*", optional = true } click = "*" cryptography = { version = "*", optional = true } -fast-query-parsers = ">=1.0.2" +fast-query-parsers = {version = ">=1.0.2", optional = true } httpx = ">=0.22" importlib-metadata = { version = "*", python = "<3.10" } importlib-resources = { version = ">=5.12.0", python = "<3.9" } @@ -180,7 +180,7 @@ prometheus = ["prometheus-client"] pydantic = ["pydantic", "pydantic-extra-types"] redis = ["redis"] sqlalchemy = ["advanced-alchemy"] -standard = ["jinja2", "jsbeautifier", "uvicorn"] +standard = ["jinja2", "jsbeautifier", "uvicorn", "fast-query-parsers"] structlog = ["structlog"] full = [ @@ -202,7 +202,8 @@ full = [ "redis", "structlog", "uvicorn", - "advanced-alchemy" + "advanced-alchemy", + "fast-query-parsers", ] [tool.poetry.scripts] diff --git a/tests/unit/test_parsers.py b/tests/unit/test_parsers.py index 6c89bacbeb..14c8e90a66 100644 --- a/tests/unit/test_parsers.py +++ b/tests/unit/test_parsers.py @@ -31,11 +31,11 @@ def test_parse_form_data() -> None: ) assert result == { "value": ["10", "12"], - "veggies": ["tomato", "potato", "aubergine"], - "nested": {"some_key": "some_value"}, + "veggies": '["tomato", "potato", "aubergine"]', + "nested": '{"some_key": "some_value"}', "calories": "122.53", - "healthy": True, - "polluting": False, + "healthy": "true", + "polluting": "false", }