diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py index 3187897380..e272c6186c 100644 --- a/django/core/handlers/asgi.py +++ b/django/core/handlers/asgi.py @@ -113,9 +113,9 @@ class ASGIRequest(HttpRequest): # Other bits. self.resolver_match = None self._parsers = [ - parsers.FormParser(), - parsers.MultiPartParser(), - parsers.JSONParser(), + parsers.FormParser, + parsers.MultiPartParser, + parsers.JSONParser, ] @cached_property diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 5518b46e2c..26a30f7398 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -79,9 +79,9 @@ class WSGIRequest(HttpRequest): self._read_started = False self.resolver_match = None self._parsers = [ - parsers.FormParser(), - parsers.MultiPartParser(), - parsers.JSONParser(), + parsers.FormParser, + parsers.MultiPartParser, + parsers.JSONParser, ] def _get_scheme(self): diff --git a/django/http/parsers.py b/django/http/parsers.py index d99a2b7d35..543b6a1f23 100644 --- a/django/http/parsers.py +++ b/django/http/parsers.py @@ -10,39 +10,50 @@ class BaseParser: media_type = None parsers = None - def can_handle(self, media_type): - return media_type == self.media_type + def __init__(self, request): + self.request = request - def parse(self, data, request=None): + @classmethod + def can_handle(cls, media_type): + return media_type == cls.media_type + + def parse(self, data): pass class FormParser(BaseParser): media_type = "application/x-www-form-urlencoded" - def parse(self, request): - from django.http import QueryDict - + def __init__(self, request): + super().__init__(request) # According to RFC 1866, the "application/x-www-form-urlencoded" # content type does not have a charset and should be always treated # as UTF-8. - if request._encoding is not None and request._encoding.lower() != "utf-8": + if ( + self.request._encoding is not None + and self.request._encoding.lower() != "utf-8" + ): raise BadRequest( "HTTP requests with the 'application/x-www-form-urlencoded' " "content type must be UTF-8 encoded." ) - return QueryDict(request.body, encoding="utf-8"), MultiValueDict() + + def parse(self, data): + from django.http import QueryDict + + return QueryDict(data, encoding="utf-8"), MultiValueDict() class MultiPartParser(BaseParser): media_type = "multipart/form-data" - def parse(self, request): + def parse(self, data): + request = self.request if hasattr(request, "_body"): # Use already read data - data = BytesIO(request._body) + request_data = BytesIO(request._body) else: - data = request + request_data = request # TODO - POST and data can be called on the same request. This parser can be # called multiple times on the same request. While `_post` `_data` are different @@ -56,7 +67,11 @@ class MultiPartParser(BaseParser): ), ) parser = _MultiPartParser( - request.META, data, request.upload_handlers, request.encoding, self.parsers + request.META, + request_data, + request.upload_handlers, + request.encoding, + self.parsers, ) # TODO _post could also be _data _post, _files = parser.parse() @@ -66,15 +81,10 @@ class MultiPartParser(BaseParser): class JSONParser(BaseParser): media_type = "application/json" - # TODO rename request -- it's not always one. - def parse(self, request): - from django.http import HttpRequest - + def parse(self, data): def strict_constant(o): raise ValueError( "Out of range float values are not JSON compliant: " + repr(o) ) - if isinstance(request, HttpRequest): - request = request.body - return json.loads(request, parse_constant=strict_constant), MultiValueDict() + return json.loads(data, parse_constant=strict_constant), MultiValueDict() diff --git a/django/http/request.py b/django/http/request.py index 77f776cd9e..16376602ec 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -70,9 +70,9 @@ class HttpRequest: self.content_type = None self.content_params = None self._parsers = [ - parsers.FormParser(), - parsers.MultiPartParser(), - parsers.JSONParser(), + parsers.FormParser, + parsers.MultiPartParser, + parsers.JSONParser, ] def __repr__(self): @@ -366,7 +366,7 @@ class HttpRequest: return if parser_list is None: - parser_list = [parsers.FormParser(), parsers.MultiPartParser()] + parser_list = [parsers.FormParser, parsers.MultiPartParser] selected_parser = None for parser in parser_list: if parser.can_handle(self.content_type): @@ -374,9 +374,13 @@ class HttpRequest: break if selected_parser: - selected_parser.parsers = parser_list + parser = selected_parser(self) try: - data, self._files = parser.parse(self) + if self.content_type == "multipart/form-data": + parser.parsers = (parser(self) for parser in parser_list) + data, self._files = parser.parse(None) + else: + data, self._files = parser.parse(self.body) setattr(self, data_attr, data) except Exception as e: # TODO 'application/x-www-form-urlencoded' didn't do this. diff --git a/tests/requests_tests/test_parsers.py b/tests/requests_tests/test_parsers.py index 9769f3b614..7c543525e1 100644 --- a/tests/requests_tests/test_parsers.py +++ b/tests/requests_tests/test_parsers.py @@ -8,11 +8,11 @@ from django.utils.http import urlencode class TestParsers(SimpleTestCase): def test_can_handle(self): - parser = MultiPartParser() + parser = MultiPartParser(HttpRequest()) self.assertIs(parser.can_handle("multipart/form-data"), True) self.assertIs(parser.can_handle("application/json"), False) - parser = FormParser() + parser = FormParser(HttpRequest()) self.assertIs(parser.can_handle("application/x-www-form-urlencoded"), True) self.assertIs(parser.can_handle("multipart/form-data"), False) @@ -24,7 +24,7 @@ class TestParsers(SimpleTestCase): main_type, sub_type = media_type.split("/") return main_type == "text" - parser = CustomParser() + parser = CustomParser(None) self.assertIs(parser.can_handle("application/json"), False) self.assertTrue(parser.can_handle("text/*"), True) self.assertTrue(parser.can_handle("text/csv"), True) @@ -32,16 +32,16 @@ class TestParsers(SimpleTestCase): def test_request_parser_no_setting(self): request = HttpRequest() form, multipart, json = request.parsers - self.assertIsInstance(form, FormParser) - self.assertIsInstance(multipart, MultiPartParser) - self.assertIsInstance(json, JSONParser) + self.assertIs(form, FormParser) + self.assertIs(multipart, MultiPartParser) + self.assertIs(json, JSONParser) def test_set_parser(self): request = HttpRequest() - request.parsers = [FormParser()] + request.parsers = [FormParser] self.assertEqual(len(request.parsers), 1) - self.assertIsInstance(request.parsers[0], FormParser) + self.assertIs(request.parsers[0], FormParser) def test_set_parsers_following_files_access(self): payload = FakePayload(urlencode({"key": "value"})) @@ -62,7 +62,7 @@ class TestParsers(SimpleTestCase): request.parsers = [] def test_json_strict(self): - parser = JSONParser() + parser = JSONParser(None) msg_base = "Out of range float values are not JSON compliant: '%s'" for value in ["Infinity", "-Infinity", "NaN"]: diff --git a/tests/requests_tests/tests.py b/tests/requests_tests/tests.py index 4f9b32d838..e51435b0d7 100644 --- a/tests/requests_tests/tests.py +++ b/tests/requests_tests/tests.py @@ -673,6 +673,34 @@ class RequestsTests(SimpleTestCase): }, ) + def test_data_form_data_json(self): + payload = FakePayload( + "\r\n".join([f"--{BOUNDARY}", *self._json_payload, f"--{BOUNDARY}--"]) + ) + request = WSGIRequest( + { + "REQUEST_METHOD": "POST", + "CONTENT_TYPE": MULTIPART_CONTENT, + "CONTENT_LENGTH": len(payload), + "wsgi.input": payload, + } + ) + self.assertEqual( + request.data, + { + "JSON": [ + { + "pk": 1, + "model": "store.book", + "fields": { + "name": "Mostly Harmless", + "author": ["Douglas", "Adams"], + }, + } + ], + }, + ) + def test_POST_multipart_json(self): payload = FakePayload( "\r\n".join( @@ -1036,6 +1064,7 @@ class RequestsTests(SimpleTestCase): ) request.body # evaluate self.assertEqual(request.POST, {"name": ["value"]}) + self.assertEqual(request.data, {"name": ["value"]}) def test_multipart_post_field_with_invalid_base64(self): payload = FakePayload( @@ -1061,6 +1090,7 @@ class RequestsTests(SimpleTestCase): ) request.body # evaluate self.assertEqual(request.POST, {"name": ["123"]}) + self.assertEqual(request.data, {"name": ["123"]}) def test_POST_after_body_read_and_stream_read_multipart(self): """ @@ -1090,6 +1120,7 @@ class RequestsTests(SimpleTestCase): # Consume enough data to mess up the parsing: self.assertEqual(request.read(13), b"--boundary\r\nC") self.assertEqual(request.POST, {"name": ["value"]}) + self.assertEqual(request.data, {"name": ["value"]}) def test_POST_immutable_for_multipart(self): """ @@ -1115,6 +1146,7 @@ class RequestsTests(SimpleTestCase): } ) self.assertFalse(request.POST._mutable) + self.assertFalse(request.data._mutable) def test_multipart_without_boundary(self): request = WSGIRequest( @@ -1129,6 +1161,10 @@ class RequestsTests(SimpleTestCase): MultiPartParserError, "Invalid boundary in multipart: None" ): request.POST + with self.assertRaisesMessage( + MultiPartParserError, "Invalid boundary in multipart: None" + ): + request.data def test_multipart_non_ascii_content_type(self): request = WSGIRequest( @@ -1145,6 +1181,8 @@ class RequestsTests(SimpleTestCase): ) with self.assertRaisesMessage(MultiPartParserError, msg): request.POST + with self.assertRaisesMessage(MultiPartParserError, msg): + request.data def test_multipart_with_header_fields_too_large(self): payload = FakePayload(