I am trying to upload a large file (≥3GB) to my FastAPI server, without loading the entire file into memory, as my server has only 2GB of free memory.
Server side:
async def uploadfiles(upload_file: UploadFile = File(...):
Client side:
m = MultipartEncoder(fields = {"upload_file":open(file_name,'rb')})
prefix = "http://xxx:5000"
url = "{}/v1/uploadfiles".format(prefix)
try:
req = requests.post(
url,
data=m,
verify=False,
)
which returns:
HTTP 422 {"detail":[{"loc":["body","upload_file"],"msg":"field required","type":"value_error.missing"}]}
I am not sure what MultipartEncoder actually sends to the server, so that the request does not match. Any ideas?
With
requests-toolbeltlibrary, you have to pass thefilenameas well, when declaring thefieldforupload_file, as well as set theContent-Typeheader—which is the main reason for the error you get, as you are sending the request without setting theContent-Typeheader tomultipart/form-data, followed by the necessaryboundarystring—as shown in the documentation. Example:However, I wouldn't recommend using a library (i.e.,
requests-toolbelt) that hasn't provided a new release for over three years now. I would suggest using Python requests instead, as demonstrated in this answer and that answer (also see Streaming Uploads and Chunk-Encoded Requests), or, preferably, use theHTTPXlibrary, which supportsasyncrequests (if you had to send multiple requests simultaneously), as well as streamingFileuploads by default, meaning that only one chunk at a time will be loaded into memory (see the documentation). Examples are given below.Option 1 (Fast) - Upload
FileandFormdata using.stream()As previously explained in detail in this answer, when you declare an
UploadFileobject, FastAPI/Starlette, under the hood, uses aSpooledTemporaryFilewith themax_sizeattribute set to 1MB, meaning that the file data is spooled in memory until the file size exceeds themax_size, at which point the contents are written to disk; more specifically, to atemporaryfile on your OS's temporary directory—see this answer on how to find/change the default temporary directory—that you later need to read the data from, using the.read()method. Hence, this whole process makes uploading file quite slow; especially, if it is a large file (as you'll see in Option 2 below later on).To avoid that and speed up the process, as the linked answer above suggested, one can access the
requestbody as a stream. As per Starlette documentation, if you use the.stream()method, the (request) byte chunks are provided without storing the entire body to memory (and later to a temporary file, if the body size exceeds 1MB). This method allows you to read and process the byte chunks as they arrive. The below takes the suggested solution a step further, by using thestreaming-form-datalibrary, which provides a Python parser for parsing streamingmultipart/form-datainput chunks. This means that not only you can uploadFormdata along withFile(s), but you also don't have to wait for the entire request body to be received, in order to start parsing the data. The way it's done is that you initialise the main parser class (passing the HTTP requestheadersthat help to determine the inputContent-Type, and hence, theboundarystring used to separate each body part in the multipart payload, etc.), and associate one of theTargetclasses to define what should be done with a field when it has been extracted out of the request body. For instance,FileTargetwould stream the data to a file on disk, whereasValueTargetwould hold the data in memory (this class can be used for eitherFormorFiledata as well, if you don't need the file(s) saved to the disk). It is also possible to define your own customTargetclasses. I have to mention thatstreaming-form-datalibrary does not currently supportasynccalls to I/O operations, meaning that the writing of chunks happenssynchronously (within adeffunction). Though, as the endpoint below uses.stream()(which is anasyncfunction), it will give up control for other tasks/requests to run on the event loop, while waiting for data to become available from the stream. You could also run the function for parsing the received data in a separate thread andawaitit, using Starlette'srun_in_threadpool()—e.g.,await run_in_threadpool(parser.data_received, chunk)—which is used by FastAPI internally when you call theasyncmethods ofUploadFile, as shown here. For more details ondefvsasync def, please have a look at this answer.You can also perform certain validation tasks, e.g., ensuring that the input size is not exceeding a certain value. This can be done using the
MaxSizeValidator. However, as this would only be applied to the fields you defined—and hence, it wouldn't prevent a malicious user from sending extremely large request body, which could result in consuming server resources in a way that the application may end up crashing—the below incorporates a customMaxBodySizeValidatorclass that is used to make sure that the request body size is not exceeding a pre-defined value. The both validators desribed above solve the problem of limiting upload file (as well as the entire request body) size in a likely better way than the one desribed here, which usesUploadFile, and hence, the file needs to be entirely received and saved to the temporary directory, before performing the check (not to mention that the approach does not take into account the request body size at all)—using as ASGI middleware such as this would be an alternative solution for limiting the request body. Also, in case you are using Gunicorn with Uvicorn, you can also define limits with regards to, for example, the number of HTTP header fields in a request, the size of an HTTP request header field, and so on (see the documentation). Similar limits can be applied when using reverse proxy servers, such as Nginx (which also allows you to set the maximum request body size using theclient_max_body_sizedirective).A few notes for the example below. Since it uses the
Requestobject directly, and notUploadFileandFormobjects, the endpoint won't be properly documented in the auto-generated docs at/docs(if that's important for your app at all). This also means that you have to perform some checks yourself, such as whether the required fields for the endpoint were received or not, and if they were in the expected format. For instance, for thedatafield, you could check whetherdata.valueis empty or not (empty would mean that the user has either not included that field in themultipart/form-data, or sent an empty value), as well as ifisinstance(data.value, str). As for the file(s), you can check whetherfile_.multipart_filenameis not empty; however, since afilenamecould likely not be included in theContent-Dispositionby some user, you may also want to check if the file exists in the filesystem, usingos.path.isfile(filepath)(Note: you need to make sure there is no pre-existing file with the same name in that specified location; otherwise, the aforementioned function would always returnTrue, even when the user did not send the file).Regarding the applied size limits, the
MAX_REQUEST_BODY_SIZEbelow must be larger thanMAX_FILE_SIZE(plus all theFormvalues size) you expcect to receive, as the raw request body (that you get from using the.stream()method) includes a few more bytes for the--boundaryandContent-Dispositionheader for each of the fields in the body. Hence, you should add a few more bytes, depending on theFormvalues and the number of files you expect to receive (hence theMAX_FILE_SIZE + 1024below).app.py
As mentioned earlier, to upload the data (on client side), you can use the
HTTPXlibrary, which supports streaming file uploads by default, and thus allows you to send large streams/files without loading them entirely into memory. You can pass additionalFormdata as well, using thedataargument. Below, a custom header, i.e.,Filename, is used to pass the filename to the server, so that the server instantiates theFileTargetclass with that name (you could use theX-prefix for custom headers, if you wish; however, it is not officially recommended anymore).test.py
Upload Multiple
Files andFormdata using.stream()To upload multiple files, use a header for each filename, or use random names on server side, and once the file is fully uploaded, you could optionally rename it to
file_.multipart_filename, for instance—regardless, in a real-world scenario, you should never trust the filename (or even the file extension) passed by the user, as it might be malicious, trying to extract or replace files in your system, and thus, it is always a good practice to add some random alphanumeric characters at the end/front of the filename, if not using a complete random name, for each file that is uploaded—and pass a list of files, as described in thehttpx's documentation. Note that you should use a different key/field name for each file, so that they don't overlap when parsing them on server side, e.g.,files = [('file0', open('bigFile.zip', 'rb')),('file1', open('otherBigFile.zip', 'rb'))]. Finally, define theTargetclasses (eitherFileTargetorValueTarget) on server side, accordingly.You could test the example below, using either the HTML template at
/, which uses JavaScript to prepare and send the request with multiple files, or the Pythonhttpxclient provided below.For simplicity purposes, the example below does not perform validation checks on the body size; however, if you wish, you could still perform those checks using the code provided in the previous example.
app.py
test.py
Upload both
Files andJSONbodyIn case you would like to upload both file(s) and JSON instead of
Formdata, you could use the approach described in Method 3 of this answer, thus also saving you from performing manual checks on the receivedFormfields, as explained earlier (see the linked answer for more details). To that end, please make the following changes in the code above. For an HTML/JS example, please refer to this answer.app.py
test.py
Option 2 (Slow) - Upload
FileandFormdata usingUploadFileandFormIf you would like to use a normal
defendpoint instead, see this answer.app.py
As mentioned earlier, using this option would take longer for the file upload to complete, and as
HTTPXuses a default timeout of 5 seconds, you will most likely get aReadTimeoutexception (as the server will need some time to read theSpooledTemporaryFilein chunks and write the contents to a permanent location on the disk). Thus, you can configure the timeout (see theTimeoutclass in the source code too), and more specifically, thereadtimeout, which "specifies the maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)". If set toNoneinstead of some positive numerical value, there will be no timeout onread.test.py