From 372049d60ddd3c32c73c99f472a3dc7404c396e5 Mon Sep 17 00:00:00 2001 From: Nancy Wang Date: Tue, 25 Nov 2025 17:26:34 +0000 Subject: [PATCH 1/4] test example --- main.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 000000000..30d4a452d --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} From 66bc00ca83b3db6bf6505c46890c6a7d9ca2e838 Mon Sep 17 00:00:00 2001 From: Nancy Wang Date: Tue, 25 Nov 2025 17:40:13 +0000 Subject: [PATCH 2/4] updated example --- main.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/main.py b/main.py index 30d4a452d..0eb81917f 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,17 @@ from typing import Union from fastapi import FastAPI +from pydantic import BaseModel app = FastAPI() +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + @app.get("/") def read_root(): return {"Hello": "World"} @@ -13,3 +20,8 @@ def read_root(): @app.get("/items/{item_id}") def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} From ce484fd61d73d61d4c1bed4e8052a4ad7a0448b0 Mon Sep 17 00:00:00 2001 From: Nancy Wang Date: Thu, 4 Dec 2025 14:45:15 +0000 Subject: [PATCH 3/4] add upload file size limit --- fastapi/applications.py | 8 +++++++- fastapi/dependencies/utils.py | 24 ++++++++++++++++++++++-- fastapi/exception_handlers.py | 8 +++++++- fastapi/exceptions.py | 5 +++++ fastapi/params.py | 2 ++ main.py | 18 +++++++++++++++++- 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 02193312b..bd6ad8988 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -20,8 +20,9 @@ from fastapi.exception_handlers import ( http_exception_handler, request_validation_exception_handler, websocket_request_validation_exception_handler, + request_entity_too_large_exception_handler, ) -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError +from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError, RequestEntityTooLarge from fastapi.logger import logger from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.openapi.docs import ( @@ -992,6 +993,11 @@ class FastAPI(Starlette): # Starlette still has incorrect type specification for the handlers websocket_request_validation_exception_handler, # type: ignore ) + self.exception_handlers.setdefault( + RequestEntityTooLarge, + request_entity_too_large_exception_handler, + ) + self.user_middleware: List[Middleware] = ( [] if middleware is None else list(middleware) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 1ff35f648..19f3dbbca 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -881,6 +881,8 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: return False +from fastapi.exceptions import RequestEntityTooLarge + async def _extract_form_body( body_fields: List[ModelField], received_body: FormData, @@ -892,10 +894,28 @@ async def _extract_form_body( field_info = field.field_info if ( isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) - and is_bytes_field(field) and isinstance(value, UploadFile) ): - value = await value.read() + #If a file size limit is defined through max_size + max_size = getattr(field_info, "max_size", None) + if max_size is not None: + CHUNK = 8192 + total = 0 + content = bytearray() + + while True: + chunk = await value.read(CHUNK) + if not chunk: + break + total += len(chunk) + if total > max_size: + raise RequestEntityTooLarge( + f"Uploaded file '{field.alias}' exceeded max size={max_size} bytes" + ) + content.extend(chunk) + value = bytes(content) + else: + value = await value.read() elif ( is_bytes_sequence_field(field) and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) diff --git a/fastapi/exception_handlers.py b/fastapi/exception_handlers.py index 475dd7bdd..9089658c3 100644 --- a/fastapi/exception_handlers.py +++ b/fastapi/exception_handlers.py @@ -1,5 +1,5 @@ from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError +from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError, RequestEntityTooLarge from fastapi.utils import is_body_allowed_for_status_code from fastapi.websockets import WebSocket from starlette.exceptions import HTTPException @@ -8,6 +8,12 @@ from starlette.responses import JSONResponse, Response from starlette.status import WS_1008_POLICY_VIOLATION +async def request_entity_too_large_exception_handler(request, exc: Exception): + return JSONResponse( + status_code=413, + content={"detail": str(exc) or "Uploaded file too large"}, + ) + async def http_exception_handler(request: Request, exc: HTTPException) -> Response: headers = getattr(exc, "headers", None) if not is_body_allowed_for_status_code(exc.status_code): diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 0620428be..215cc0b72 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -182,3 +182,8 @@ class ResponseValidationError(ValidationException): for err in self._errors: message += f" {err}\n" return message + +class RequestEntityTooLarge(Exception): + """Raised when uploaded content exceeds the configured max_size.""" + pass + diff --git a/fastapi/params.py b/fastapi/params.py index 6d07df35e..9ed5a5ba7 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -725,6 +725,7 @@ class File(Form): # type: ignore[misc] deprecated: Union[deprecated, str, bool, None] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, + max_size: Optional[int] = None, **extra: Any, ): super().__init__( @@ -760,6 +761,7 @@ class File(Form): # type: ignore[misc] json_schema_extra=json_schema_extra, **extra, ) + self.max_size = max_size @dataclass(frozen=True) diff --git a/main.py b/main.py index 0eb81917f..2d025ac83 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from typing import Union -from fastapi import FastAPI +from fastapi import FastAPI, UploadFile, File from pydantic import BaseModel app = FastAPI() @@ -25,3 +25,19 @@ def read_item(item_id: int, q: Union[str, None] = None): @app.put("/items/{item_id}") def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} + + +@app.post("/upload") +async def upload_file(file: UploadFile = File(max_size=500)): + total_bytes = 0 + + # Safest: process in chunks instead of reading whole file + while True: + chunk = await file.read(1024 * 1024) # 1MB + if not chunk: + break + total_bytes += len(chunk) + + return {"filename": file.filename, "size": total_bytes} + + From 468c1437e6ad0efcfb25c6e95fca76d215bafcd7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:37:31 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 15 +++++++++------ fastapi/dependencies/utils.py | 15 +++++++-------- fastapi/exception_handlers.py | 6 +++++- fastapi/exceptions.py | 3 ++- main.py | 6 ++---- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index bd6ad8988..889e14c93 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -18,11 +18,15 @@ from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.exception_handlers import ( http_exception_handler, + request_entity_too_large_exception_handler, request_validation_exception_handler, websocket_request_validation_exception_handler, - request_entity_too_large_exception_handler, ) -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError, RequestEntityTooLarge +from fastapi.exceptions import ( + RequestEntityTooLarge, + RequestValidationError, + WebSocketRequestValidationError, +) from fastapi.logger import logger from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.openapi.docs import ( @@ -994,10 +998,9 @@ class FastAPI(Starlette): websocket_request_validation_exception_handler, # type: ignore ) self.exception_handlers.setdefault( - RequestEntityTooLarge, - request_entity_too_large_exception_handler, - ) - + RequestEntityTooLarge, + request_entity_too_large_exception_handler, + ) self.user_middleware: List[Middleware] = ( [] if middleware is None else list(middleware) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 19f3dbbca..7f63210ce 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -36,7 +36,6 @@ from fastapi._compat import ( get_annotation_from_field_info, get_cached_model_fields, get_missing_field_error, - is_bytes_field, is_bytes_sequence_field, is_scalar_field, is_scalar_sequence_field, @@ -883,6 +882,7 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: from fastapi.exceptions import RequestEntityTooLarge + async def _extract_form_body( body_fields: List[ModelField], received_body: FormData, @@ -892,11 +892,10 @@ async def _extract_form_body( for field in body_fields: value = _get_multidict_value(field, received_body) field_info = field.field_info - if ( - isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) - and isinstance(value, UploadFile) - ): - #If a file size limit is defined through max_size + if isinstance( + field_info, (params.File, temp_pydantic_v1_params.File) + ) and isinstance(value, UploadFile): + # If a file size limit is defined through max_size max_size = getattr(field_info, "max_size", None) if max_size is not None: CHUNK = 8192 @@ -910,8 +909,8 @@ async def _extract_form_body( total += len(chunk) if total > max_size: raise RequestEntityTooLarge( - f"Uploaded file '{field.alias}' exceeded max size={max_size} bytes" - ) + f"Uploaded file '{field.alias}' exceeded max size={max_size} bytes" + ) content.extend(chunk) value = bytes(content) else: diff --git a/fastapi/exception_handlers.py b/fastapi/exception_handlers.py index 9089658c3..dadbf2680 100644 --- a/fastapi/exception_handlers.py +++ b/fastapi/exception_handlers.py @@ -1,5 +1,8 @@ from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError, RequestEntityTooLarge +from fastapi.exceptions import ( + RequestValidationError, + WebSocketRequestValidationError, +) from fastapi.utils import is_body_allowed_for_status_code from fastapi.websockets import WebSocket from starlette.exceptions import HTTPException @@ -14,6 +17,7 @@ async def request_entity_too_large_exception_handler(request, exc: Exception): content={"detail": str(exc) or "Uploaded file too large"}, ) + async def http_exception_handler(request: Request, exc: HTTPException) -> Response: headers = getattr(exc, "headers", None) if not is_body_allowed_for_status_code(exc.status_code): diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 215cc0b72..e922ed948 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -183,7 +183,8 @@ class ResponseValidationError(ValidationException): message += f" {err}\n" return message + class RequestEntityTooLarge(Exception): """Raised when uploaded content exceeds the configured max_size.""" - pass + pass diff --git a/main.py b/main.py index 2d025ac83..970698837 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from typing import Union -from fastapi import FastAPI, UploadFile, File +from fastapi import FastAPI, File, UploadFile from pydantic import BaseModel app = FastAPI() @@ -30,7 +30,7 @@ def update_item(item_id: int, item: Item): @app.post("/upload") async def upload_file(file: UploadFile = File(max_size=500)): total_bytes = 0 - + # Safest: process in chunks instead of reading whole file while True: chunk = await file.read(1024 * 1024) # 1MB @@ -39,5 +39,3 @@ async def upload_file(file: UploadFile = File(max_size=500)): total_bytes += len(chunk) return {"filename": file.filename, "size": total_bytes} - -