Skip to content

Commit 7732f94

Browse files
FarhanAliRazajacobtylerwalls
authored andcommitted
Fixed #36841 -- Made multipart parser class pluggable on HttpRequest.
1 parent 56ed37e commit 7732f94

4 files changed

Lines changed: 114 additions & 3 deletions

File tree

django/http/request.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class HttpRequest:
5656
# The encoding used in GET/POST dicts. None means use default setting.
5757
_encoding = None
5858
_upload_handlers = []
59+
_multipart_parser_class = MultiPartParser
5960

6061
def __init__(self):
6162
# WARNING: The `WSGIRequest` subclass doesn't call `super`.
@@ -364,6 +365,19 @@ def upload_handlers(self, upload_handlers):
364365
)
365366
self._upload_handlers = upload_handlers
366367

368+
@property
369+
def multipart_parser_class(self):
370+
return self._multipart_parser_class
371+
372+
@multipart_parser_class.setter
373+
def multipart_parser_class(self, multipart_parser_class):
374+
if hasattr(self, "_files"):
375+
raise RuntimeError(
376+
"You cannot set the multipart parser class after the upload has been "
377+
"processed."
378+
)
379+
self._multipart_parser_class = multipart_parser_class
380+
367381
def parse_file_upload(self, META, post_data):
368382
"""Return a tuple of (POST QueryDict, FILES MultiValueDict)."""
369383
self.upload_handlers = ImmutableList(
@@ -373,7 +387,9 @@ def parse_file_upload(self, META, post_data):
373387
"processed."
374388
),
375389
)
376-
parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
390+
parser = self.multipart_parser_class(
391+
META, post_data, self.upload_handlers, self.encoding
392+
)
377393
return parser.parse()
378394

379395
@property

docs/ref/request-response.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,30 @@ All attributes should be considered read-only, unless stated otherwise.
218218
executed before URL resolving takes place (you can use it in
219219
:meth:`process_view` though).
220220

221+
.. attribute:: HttpRequest.multipart_parser_class
222+
223+
.. versionadded:: 6.1
224+
225+
The class used to parse ``multipart/form-data`` request data. By default,
226+
this is ``django.http.multipartparser.MultiPartParser``.
227+
228+
You can set this attribute to use a custom multipart parser, either via
229+
middleware or directly in views::
230+
231+
from django.http.multipartparser import MultiPartParser
232+
233+
234+
class CustomMultiPartParser(MultiPartParser):
235+
def parse(self):
236+
post = QueryDict(mutable=True)
237+
files = MultiValueDict()
238+
# Custom processing logic here
239+
return post, files
240+
241+
242+
# In middleware or view:
243+
request.multipart_parser_class = CustomMultiPartParser
244+
221245
Attributes set by application code
222246
----------------------------------
223247

docs/releases/6.1.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ Pagination
295295
Requests and Responses
296296
~~~~~~~~~~~~~~~~~~~~~~
297297

298-
* ...
298+
* :attr:`HttpRequest.multipart_parser_class <django.http.HttpRequest.multipart_parser_class>`
299+
can now be customized to use a different multipart parser class.
299300

300301
Security
301302
~~~~~~~~

tests/requests_tests/tests.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
RawPostDataException,
1414
UnreadablePostError,
1515
)
16-
from django.http.multipartparser import MAX_TOTAL_HEADER_SIZE, MultiPartParserError
16+
from django.http.multipartparser import (
17+
MAX_TOTAL_HEADER_SIZE,
18+
MultiPartParser,
19+
MultiPartParserError,
20+
)
1721
from django.http.request import split_domain_port
1822
from django.test import RequestFactory, SimpleTestCase, override_settings
1923
from django.test.client import BOUNDARY, MULTIPART_CONTENT, FakePayload
@@ -1112,6 +1116,72 @@ def test_deepcopy(self):
11121116
request.session["key"] = "value"
11131117
self.assertEqual(request_copy.session, {})
11141118

1119+
def test_custom_multipart_parser_class(self):
1120+
1121+
class CustomMultiPartParser(MultiPartParser):
1122+
def parse(self):
1123+
post, files = super().parse()
1124+
post._mutable = True
1125+
post["custom_parser_used"] = "yes"
1126+
post._mutable = False
1127+
return post, files
1128+
1129+
class CustomWSGIRequest(WSGIRequest):
1130+
multipart_parser_class = CustomMultiPartParser
1131+
1132+
payload = FakePayload(
1133+
"\r\n".join(
1134+
[
1135+
"--boundary",
1136+
'Content-Disposition: form-data; name="name"',
1137+
"",
1138+
"value",
1139+
"--boundary--",
1140+
]
1141+
)
1142+
)
1143+
request = CustomWSGIRequest(
1144+
{
1145+
"REQUEST_METHOD": "POST",
1146+
"CONTENT_TYPE": "multipart/form-data; boundary=boundary",
1147+
"CONTENT_LENGTH": len(payload),
1148+
"wsgi.input": payload,
1149+
}
1150+
)
1151+
self.assertEqual(request.POST.get("custom_parser_used"), "yes")
1152+
self.assertEqual(request.POST.get("name"), "value")
1153+
1154+
def test_multipart_parser_class_immutable_after_parse(self):
1155+
payload = FakePayload(
1156+
"\r\n".join(
1157+
[
1158+
"--boundary",
1159+
'Content-Disposition: form-data; name="name"',
1160+
"",
1161+
"value",
1162+
"--boundary--",
1163+
]
1164+
)
1165+
)
1166+
request = WSGIRequest(
1167+
{
1168+
"REQUEST_METHOD": "POST",
1169+
"CONTENT_TYPE": "multipart/form-data; boundary=boundary",
1170+
"CONTENT_LENGTH": len(payload),
1171+
"wsgi.input": payload,
1172+
}
1173+
)
1174+
1175+
# Access POST to trigger parsing.
1176+
request.POST
1177+
1178+
msg = (
1179+
"You cannot set the multipart parser class after the upload has been "
1180+
"processed."
1181+
)
1182+
with self.assertRaisesMessage(RuntimeError, msg):
1183+
request.multipart_parser_class = MultiPartParser
1184+
11151185

11161186
class HostValidationTests(SimpleTestCase):
11171187
poisoned_hosts = [

0 commit comments

Comments
 (0)