Request files are normally sent as multipart form data (`multipart/form-data`).
-Signature: `request.form(max_files=1000, max_fields=1000)`
+Signature: `request.form(max_files=1000, max_fields=1000, max_part_size=1024*1024)`
-You can configure the number of maximum fields or files with the parameters `max_files` and `max_fields`:
+You can configure the number of maximum fields or files with the parameters `max_files` and `max_fields`; and part size using `max_part_size`:
```python
-async with request.form(max_files=1000, max_fields=1000):
+async with request.form(max_files=1000, max_fields=1000, max_part_size=1024*1024):
...
```
class MultiPartParser:
max_file_size = 1024 * 1024 # 1MB
- max_part_size = 1024 * 1024 # 1MB
def __init__(
self,
*,
max_files: int | float = 1000,
max_fields: int | float = 1000,
+ max_part_size: int = 1024 * 1024, # 1MB
) -> None:
assert multipart is not None, "The `python-multipart` library must be installed to use form parsing."
self.headers = headers
self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = []
self._file_parts_to_finish: list[MultipartPart] = []
self._files_to_close_on_error: list[SpooledTemporaryFile[bytes]] = []
+ self.max_part_size = max_part_size
def on_part_begin(self) -> None:
self._current_part = MultipartPart()
self._json = json.loads(body)
return self._json
- async def _get_form(self, *, max_files: int | float = 1000, max_fields: int | float = 1000) -> FormData:
+ async def _get_form(
+ self,
+ *,
+ max_files: int | float = 1000,
+ max_fields: int | float = 1000,
+ max_part_size: int = 1024 * 1024,
+ ) -> FormData:
if self._form is None: # pragma: no branch
assert (
parse_options_header is not None
self.stream(),
max_files=max_files,
max_fields=max_fields,
+ max_part_size=max_part_size,
)
self._form = await multipart_parser.parse()
except MultiPartException as exc:
return self._form
def form(
- self, *, max_files: int | float = 1000, max_fields: int | float = 1000
+ self,
+ *,
+ max_files: int | float = 1000,
+ max_fields: int | float = 1000,
+ max_part_size: int = 1024 * 1024,
) -> AwaitableOrContextManager[FormData]:
- return AwaitableOrContextManagerWrapper(self._get_form(max_files=max_files, max_fields=max_fields))
+ return AwaitableOrContextManagerWrapper(
+ self._get_form(max_files=max_files, max_fields=max_fields, max_part_size=max_part_size)
+ )
async def close(self) -> None:
if self._form is not None: # pragma: no branch
await response(scope, receive, send)
-def make_app_max_parts(max_files: int = 1000, max_fields: int = 1000) -> ASGIApp:
+def make_app_max_parts(max_files: int = 1000, max_fields: int = 1000, max_part_size: int = 1024 * 1024) -> ASGIApp:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope, receive)
- data = await request.form(max_files=max_files, max_fields=max_fields)
+ data = await request.form(max_files=max_files, max_fields=max_fields, max_part_size=max_part_size)
output: dict[str, typing.Any] = {}
for key, value in data.items():
if isinstance(value, UploadFile):
response = client.post("/", data=multipart_data, headers=headers) # type: ignore
assert response.status_code == 400
assert response.text == "Part exceeded maximum size of 1024KB."
+
+
+@pytest.mark.parametrize(
+ "app,expectation",
+ [
+ (make_app_max_parts(max_part_size=1024 * 10), pytest.raises(MultiPartException)),
+ (
+ Starlette(routes=[Mount("/", app=make_app_max_parts(max_part_size=1024 * 10))]),
+ does_not_raise(),
+ ),
+ ],
+)
+def test_max_part_size_exceeds_custom_limit(
+ app: ASGIApp,
+ expectation: typing.ContextManager[Exception],
+ test_client_factory: TestClientFactory,
+) -> None:
+ client = test_client_factory(app)
+ boundary = "------------------------4K1ON9fZkj9uCUmqLHRbbR"
+
+ multipart_data = (
+ f"--{boundary}\r\n"
+ f'Content-Disposition: form-data; name="small"\r\n\r\n'
+ "small content\r\n"
+ f"--{boundary}\r\n"
+ f'Content-Disposition: form-data; name="large"\r\n\r\n'
+ + ("x" * 1024 * 10 + "x") # 1MB + 1 byte of data
+ + "\r\n"
+ f"--{boundary}--\r\n"
+ ).encode("utf-8")
+
+ headers = {
+ "Content-Type": f"multipart/form-data; boundary={boundary}",
+ "Transfer-Encoding": "chunked",
+ }
+
+ with expectation:
+ response = client.post("/", content=multipart_data, headers=headers)
+ assert response.status_code == 400
+ assert response.text == "Part exceeded maximum size of 10KB."