@@ -1911,16 +1911,47 @@ async def upload_file(request: Request):
19111911 """
19121912 from reflex .utils .exceptions import UploadTypeError , UploadValueError
19131913
1914+ config = get_config ()
1915+ upload_max_size = config .upload_max_size
1916+ upload_max_files = config .upload_max_files
1917+
1918+ # Reject based on Content-Length before Starlette buffers the body.
1919+ # Content-Length covers the entire multipart request, so use
1920+ # (per-file limit * max files) as the upper bound.
1921+ if upload_max_size > 0 and upload_max_files > 0 :
1922+ content_length_str = request .headers .get ("content-length" )
1923+ if content_length_str is not None :
1924+ try :
1925+ content_length = int (content_length_str )
1926+ except ValueError :
1927+ return JSONResponse (
1928+ status_code = 400 ,
1929+ content = {"detail" : "Invalid Content-Length header." },
1930+ )
1931+ max_request_size = upload_max_size * upload_max_files
1932+ if content_length > max_request_size :
1933+ return JSONResponse (
1934+ status_code = 413 ,
1935+ content = {"detail" : f"Upload exceeds the maximum allowed size of { max_request_size } bytes." },
1936+ )
1937+
19141938 # Get the files from the request.
19151939 try :
19161940 files = await request .form ()
19171941 except ClientDisconnect :
19181942 return Response () # user cancelled
1919- files = files .getlist ("files" )
1920- if not files :
1943+ file_list = files .getlist ("files" )
1944+ if not file_list :
19211945 msg = "No files were uploaded."
19221946 raise UploadValueError (msg )
19231947
1948+ # Enforce max file count.
1949+ if upload_max_files > 0 and len (file_list ) > upload_max_files :
1950+ return JSONResponse (
1951+ status_code = 400 ,
1952+ content = {"detail" : f"Too many files uploaded ({ len (file_list )} ). Maximum allowed is { upload_max_files } ." },
1953+ )
1954+
19241955 token = request .headers .get ("reflex-client-token" )
19251956 handler = request .headers .get ("reflex-event-handler" )
19261957
@@ -1966,19 +1997,54 @@ async def upload_file(request: Request):
19661997 )
19671998 raise UploadValueError (msg )
19681999
2000+ async def _cleanup (upload_files , copied_files ):
2001+ """Close all uploaded files and any already-copied BytesIO buffers.
2002+
2003+ Args:
2004+ upload_files: The raw uploaded file list from the request.
2005+ copied_files: The list of UploadFile copies made so far.
2006+ """
2007+ for f in upload_files :
2008+ if isinstance (f , StarletteUploadFile ):
2009+ await f .close ()
2010+ for f in copied_files :
2011+ f .file .close ()
2012+
19692013 # Make a copy of the files as they are closed after the request.
19702014 # This behaviour changed from fastapi 0.103.0 to 0.103.1 as the
19712015 # AsyncExitStack was removed from the request scope and is now
19722016 # part of the routing function which closes this before the
19732017 # event is handled.
19742018 file_copies = []
1975- for file in files :
2019+ for file in file_list :
19762020 if not isinstance (file , StarletteUploadFile ):
2021+ await _cleanup (file_list , file_copies )
19772022 raise UploadValueError (
19782023 "Uploaded file is not an UploadFile." + str (file )
19792024 )
2025+ # Enforce upload size limit: early rejection via file.size header
2026+ if upload_max_size > 0 and file .size is not None and file .size > upload_max_size :
2027+ await _cleanup (file_list , file_copies )
2028+ return JSONResponse (
2029+ status_code = 413 ,
2030+ content = {"detail" : f"File exceeds the maximum upload size of { upload_max_size } bytes." },
2031+ )
19802032 content_copy = io .BytesIO ()
1981- content_copy .write (await file .read ())
2033+ # Read in chunks to enforce limit even when file.size is not reported
2034+ bytes_read = 0
2035+ while True :
2036+ chunk = await file .read (1024 * 1024 ) # 1 MB chunks
2037+ if not chunk :
2038+ break
2039+ bytes_read += len (chunk )
2040+ if upload_max_size > 0 and bytes_read > upload_max_size :
2041+ content_copy .close ()
2042+ await _cleanup (file_list , file_copies )
2043+ return JSONResponse (
2044+ status_code = 413 ,
2045+ content = {"detail" : f"File exceeds the maximum upload size of { upload_max_size } bytes." },
2046+ )
2047+ content_copy .write (chunk )
19822048 content_copy .seek (0 )
19832049 file_copies .append (
19842050 UploadFile (
@@ -1989,12 +2055,10 @@ async def upload_file(request: Request):
19892055 )
19902056 )
19912057
1992- for file in files :
1993- if not isinstance (file , StarletteUploadFile ):
1994- raise UploadValueError (
1995- "Uploaded file is not an UploadFile." + str (file )
1996- )
1997- await file .close ()
2058+ # Close the raw uploaded files (copies are kept for event processing).
2059+ for file in file_list :
2060+ if isinstance (file , StarletteUploadFile ):
2061+ await file .close ()
19982062
19992063 event = Event (
20002064 token = token ,
0 commit comments