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-toolbelt library, you have to pass the filename as well, when declaring the field for upload_file, as well as set the Content-Type header—which is the main reason for the error you get, as you are sending the request without setting the Content-Type header to multipart/form-data, followed by the necessary boundary string—as shown in the documentation. Example:
filename = 'my_file.txt'
m = MultipartEncoder(fields={'upload_file': (filename, open(filename, 'rb'))})
r = requests.post(url, data=m, headers={'Content-Type': m.content_type})
print(r.request.headers) # confirm that the 'Content-Type' header has been set
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 the HTTPX library, which supports async requests (if you had to send multiple requests simultaneously), as well as streaming File uploads 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 File and Form data using .stream()
As previously explained in detail in this answer, when you declare an UploadFile object, FastAPI/Starlette, under the hood, uses a SpooledTemporaryFile with the max_size attribute set to 1MB, meaning that the file data is spooled in memory until the file size exceeds the max_size, at which point the contents are written to disk; more specifically, to a temporary file 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 request body 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 the streaming-form-data library, which provides a Python parser for parsing streaming multipart/form-data input chunks. This means that not only you can upload Form data along with File(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 request headers that help to determine the input Content-Type, and hence, the boundary string used to separate each body part in the multipart payload, etc.), and associate one of the Target classes to define what should be done with a field when it has been extracted out of the request body. For instance, FileTarget would stream the data to a file on disk, whereas ValueTarget would hold the data in memory (this class can be used for either Form or File data as well, if you don't need the file(s) saved to the disk). It is also possible to define your own custom Target classes. I have to mention that streaming-form-data library does not currently support async calls to I/O operations, meaning that the writing of chunks happens synchronously (within a def function). Though, as the endpoint below uses .stream() (which is an async function), 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 and await it, using Starlette's run_in_threadpool()—e.g., await run_in_threadpool(parser.data_received, chunk)—which is used by FastAPI internally when you call the async methods of UploadFile, as shown here. For more details on def vs async 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 custom MaxBodySizeValidator class 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 uses UploadFile, 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 the client_max_body_size directive).
A few notes for the example below. Since it uses the Request object directly, and not UploadFile and Form objects, 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 the data field, you could check whether data.value is empty or not (empty would mean that the user has either not included that field in the multipart/form-data, or sent an empty value), as well as if isinstance(data.value, str). As for the file(s), you can check whether file_.multipart_filename is not empty; however, since a filename could likely not be included in the Content-Disposition by some user, you may also want to check if the file exists in the filesystem, using os.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 return True, even when the user did not send the file).
Regarding the applied size limits, the MAX_REQUEST_BODY_SIZE below must be larger than MAX_FILE_SIZE (plus all the Form values 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 --boundary and Content-Disposition header for each of the fields in the body. Hence, you should add a few more bytes, depending on the Form values and the number of files you expect to receive (hence the MAX_FILE_SIZE + 1024 below).
app.py
from fastapi import FastAPI, Request, HTTPException, status
from streaming_form_data import StreamingFormDataParser
from streaming_form_data.targets import FileTarget, ValueTarget
from streaming_form_data.validators import MaxSizeValidator
import streaming_form_data
from starlette.requests import ClientDisconnect
import os
MAX_FILE_SIZE = 1024 * 1024 * 1024 * 4 # = 4GB
MAX_REQUEST_BODY_SIZE = MAX_FILE_SIZE + 1024
app = FastAPI()
class MaxBodySizeException(Exception):
def __init__(self, body_len: str):
self.body_len = body_len
class MaxBodySizeValidator:
def __init__(self, max_size: int):
self.body_len = 0
self.max_size = max_size
def __call__(self, chunk: bytes):
self.body_len += len(chunk)
if self.body_len > self.max_size:
raise MaxBodySizeException(body_len=self.body_len)
#app.post('/upload')
async def upload(request: Request):
body_validator = MaxBodySizeValidator(MAX_REQUEST_BODY_SIZE)
filename = request.headers.get('Filename')
if not filename:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail='Filename header is missing')
try:
filepath = os.path.join('./', os.path.basename(filename))
file_ = FileTarget(filepath, validator=MaxSizeValidator(MAX_FILE_SIZE))
data = ValueTarget()
parser = StreamingFormDataParser(headers=request.headers)
parser.register('file', file_)
parser.register('data', data)
async for chunk in request.stream():
body_validator(chunk)
parser.data_received(chunk)
except ClientDisconnect:
print("Client Disconnected")
except MaxBodySizeException as e:
raise HTTPException(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f'Maximum request body size limit ({MAX_REQUEST_BODY_SIZE} bytes) exceeded ({e.body_len} bytes read)')
except streaming_form_data.validators.ValidationError:
raise HTTPException(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f'Maximum file size limit ({MAX_FILE_SIZE} bytes) exceeded')
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='There was an error uploading the file')
if not file_.multipart_filename:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail='File is missing')
print(data.value.decode())
print(file_.multipart_filename)
return {"message": f"Successfuly uploaded {filename}"}
As mentioned earlier, to upload the data (on client side), you can use the HTTPX library, 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 additional Form data as well, using the data argument. Below, a custom header, i.e., Filename, is used to pass the filename to the server, so that the server instantiates the FileTarget class with that name (you could use the X- prefix for custom headers, if you wish; however, it is not officially recommended anymore).
To upload multiple files, use a header for each file (or, use random names on server side, and once the file has been fully uploaded, you can optionally rename it using the file_.multipart_filename attribute), pass a list of files, as described in the documentation (Note: use a different field name for each file, so that they won't overlap when parsing them on server side, e.g., files = [('file', open('bigFile.zip', 'rb')),('file_2', open('bigFile2.zip', 'rb'))], and finally, define the Target classes on server side accordingly.
test.py
import httpx
import time
url ='http://127.0.0.1:8000/upload'
files = {'file': open('bigFile.zip', 'rb')}
headers={'Filename': 'bigFile.zip'}
data = {'data': 'Hello World!'}
with httpx.Client() as client:
start = time.time()
r = client.post(url, data=data, files=files, headers=headers)
end = time.time()
print(f'Time elapsed: {end - start}s')
print(r.status_code, r.json(), sep=' ')
Upload both File and JSON body
In case you would like to upload both file(s) and JSON instead of Form data, you can use the approach described in Method 3 of this answer, thus also saving you from performing manual checks on the received Form fields, as explained earlier (see the linked answer for more details). To do that, make the following changes in the code above.
app.py
#...
from fastapi import Form
from pydantic import BaseModel, ValidationError
from typing import Optional
from fastapi.encoders import jsonable_encoder
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
def checker(data: str = Form(...)):
try:
model = Base.parse_raw(data)
except ValidationError as e:
raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
return model
#...
#app.post('/upload')
async def upload(request: Request):
#...
# place this after the try-except block
model = checker(data.value.decode())
print(model.dict())
test.py
#...
import json
data = {'data': json.dumps({"name": "foo", "point": 0.13, "is_accepted": False})}
#...
Option 2 (Slow) - Upload File and Form data using UploadFile and Form
If you would like to use a normal def endpoint instead, see this answer.
app.py
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, status
import aiofiles
import os
CHUNK_SIZE = 1024 * 1024 # adjust the chunk size as desired
app = FastAPI()
#app.post("/upload")
async def upload(file: UploadFile = File(...), data: str = Form(...)):
try:
filepath = os.path.join('./', os.path.basename(file.filename))
async with aiofiles.open(filepath, 'wb') as f:
while chunk := await file.read(CHUNK_SIZE):
await f.write(chunk)
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='There was an error uploading the file')
finally:
await file.close()
return {"message": f"Successfuly uploaded {file.filename}"}
As mentioned earlier, using this option would take longer for the file upload to complete, and as HTTPX uses a default timeout of 5 seconds, you will most likely get a ReadTimeout exception (as the server will need some time to read the SpooledTemporaryFile in chunks and write the contents to a permanent location on the disk). Thus, you can configure the timeout (see the Timeout class in the source code too), and more specifically, the read timeout, 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 to None instead of some positive numerical value, there will be no timeout on read.
test.py
import httpx
import time
url ='http://127.0.0.1:8000/upload'
files = {'file': open('bigFile.zip', 'rb')}
headers={'Filename': 'bigFile.zip'}
data = {'data': 'Hello World!'}
timeout = httpx.Timeout(None, read=180.0)
with httpx.Client(timeout=timeout) as client:
start = time.time()
r = client.post(url, data=data, files=files, headers=headers)
end = time.time()
print(f'Time elapsed: {end - start}s')
print(r.status_code, r.json(), sep=' ')
So I have a Python script that sends a get request to an API and returns information. For educational purposes, I wanted to create an API that I could pass parameters to which would return results from that script. I couldn't really find a similar example on stackoverflow or on the official flask documentation for an example that meets what I'm looking for. Everything I'm seeing is for returning data that you already have existing in a database or in a json file.
Here's an example of the script that I have right now.
script.py
api_url = www.statetaxes.com
parameters = {"taxinfo": "state:Texas county:soaker_county zip_code:78331"}
token = "abcd123456"
response= request.get(api_url, params=parameters, headers={"authentication":token})
print(response.json()) >----- [{tax rate: 1.5%, education_quality:great, crime_rate:0}]
The user would set the parameters in a config file somewhere like so:
[taxinfo]
county: some_county
zip_code: 12345
I'd want to read that text file, grab those parameters and push it to an API endpoint. I can figure out the part on how to read those parameters and send it to the API, but I'm not sure how I'm supposed to be configuring the rest of my FLASK app. I'd assume that I'd have to take those parameters which I'm reading from the text file as an argument. Am I on the right path here?
#app.route('/taxes/<taxinfo>', methods = ['GET', 'POST', 'PUT' ])
def state_taxes(taxinfo):
def api_call():
api_url = www.statetaxes.com
parameters=taxinfo
token = "abcd123456"
response= request.get(api_url, params=parameters, headers={"authentication":token})
return api_call
if __name__== "__main__":
app.run(debug=True)
This simple Falcon API will take a HTTP POST with enctype=multipart/form-data and a file upload in the file parameter and print the file's content on the console:
# simple_api.py
import cgi
import falcon
class SomeTestApi(object):
def on_post(self, req, resp):
upload = cgi.FieldStorage(fp=req.stream, environ=req.env)
upload = upload['file'].file.read()
print(upload)
app = falcon.API()
app.add_route('/', SomeTestApi())
One might also use the falcon-multipart middleware to achieve the same goal.
To try it out, run it e.g. with gunicorn (pip install gunicorn),
gunicorn simple_api.py
then use cUrl (or any REST client of choice) to upload a text file:
# sample.txt
this is some sample text
curl -F "file=#sample.txt" localhost:8000
I would like to test this API now with Falcon's testing helpers by simulating a file upload. However, I do not understand yet how to do this (if it is possible at all?). The simulate_request method has a file_wrapper parameter which might be useful but from the documentation I do not understand how this is supposed to be filled.
Any suggestions?
This is what I came up with, which tries to simulate what my Chrome does.
Note that this simulates the case when you are uploading only one file, but you can simply modify this function to upload multiple files, each one separated by two new lines.
def create_multipart(data, fieldname, filename, content_type):
"""
Basic emulation of a browser's multipart file upload
"""
boundry = '----WebKitFormBoundary' + random_string(16)
buff = io.BytesIO()
buff.write(b'--')
buff.write(boundry.encode())
buff.write(b'\r\n')
buff.write(('Content-Disposition: form-data; name="%s"; filename="%s"' % \
(fieldname, filename)).encode())
buff.write(b'\r\n')
buff.write(('Content-Type: %s' % content_type).encode())
buff.write(b'\r\n')
buff.write(b'\r\n')
buff.write(data)
buff.write(b'\r\n')
buff.write(boundry.encode())
buff.write(b'--\r\n')
headers = {'Content-Type': 'multipart/form-data; boundary=%s' %boundry}
headers['Content-Length'] = str(buff.tell())
return buff.getvalue(), headers
You can then use this function like the following:
with open('test/resources/foo.pdf', 'rb') as f:
foodata = f.read()
# Create the multipart data
data, headers = create_multipart(foodata, fieldname='uploadFile',
filename='foo.pdf',
content_type='application/pdf')
# Post to endpoint
client.simulate_request(method='POST', path=url,
headers=headers, body=data)
You can craft a suitable request body and Content-Type using the encode_multipart_formdata function in urllib3, documented here. An example usage:
from falcon import testing
import pytest
import myapp
import urllib3
# Depending on your testing strategy and how your application
# manages state, you may be able to broaden the fixture scope
# beyond the default 'function' scope used in this example.
#pytest.fixture()
def client():
# Assume the hypothetical `myapp` package has a function called
# `create()` to initialize and return a `falcon.App` instance.
return testing.TestClient(myapp.create())
# a dictionary mapping the HTML form label to the file uploads
fields = {
'file_1_form_label': ( # label in HTML form object
'file1.txt', # filename
open('path/to/file1.txt').read(), # file contents
'text/plain' # MIME type
),
'file_2_form_label': (
'file2.json',
open('path/to/file2.json').read(),
'application/json'
)
}
# create the body and header
body, content_type_header = urllib3.encode_multipart_formdata(fields)
# NOTE: modify these headers to reflect those generated by your browser
# and/or required by the falcon application you're testing
headers = {
'Content-Type': content_type_header,
}
# craft the mock query using the falcon testing framework
response = client.simulate_request(
method="POST",
path='/app_path',
headers=headers,
body=body)
print(response.status_code)
Note the syntax of the fields object, which is used as input for the encode_multipart_formdata function.
See Tim Head's blog post for another example:
https://betatim.github.io/posts/python-create-multipart-formdata/
Falcon testing example copied from their docs:
https://falcon.readthedocs.io/en/stable/api/testing.html
I'm writing a fairly small lightweight REST api so I chose restless as the quickest/easiest support for that. I didn't seem to need all the complexity and support of the django-REST module. My service will only received and send json but users need to upload files to one single endpoint. Currently my view/api code for the file upload looks like:
class SubmissionCreate(Endpoint):
def post(self, request):
# get the data from the post request
data = {}
data['input_data'] = request.FILES['input_data'].read().decode('UTF-8')
data['submission_name'] = request.FILES['submission_name'].read().decode('UTF-8')
submission_form = SubmissionForm(data)
if submission_form.is_valid():
s = submission_form.save()
return {'message': 'file uploaded'}
else:
return {'error': 'Input information is not correctly formatted'}
I also wrote a small client with Requests to upload files
import os
import requests
import json
url = 'http://127.0.0.1:8000/submission/create/'
payload = {'input_data': ('input.txt', open('./static/files/file1.txt', 'rb')), 'submission_name': 'test'}
r = requests.post(url, files=payload)
This works great and I can push files to the database with my client. But obviously I want some proper testing before I give this any more complex behaviour, so I looked at the docs and wrote the following test
class SubmissionCreateTests(TestCase):
def test_submissioncreate_will_accept_data(self):
f = SimpleUploadedFile("file.txt", bytes("file_content", 'utf-8'))
response = self.client.post(reverse('submission_data'),
{'input_data': f, 'submission_name': 'test'})
self.assertEqual(response.status_code, 200)
However this produces the following error:
django.utils.datastructures.MultiValueDictKeyError: "'submission_name'"
If I set the content_type to 'application/x-www-form-urlencoded' I get the same error.
If I set the content_type to 'multipart/form-data' I get a 400 error but the tests run with no exception thrown.
I tried to fix this but in the end it was quicker and easier to switch to the Django-REST framework. The docs are so much better for Djano-REST that it was trivial to set it up and build tests. There seem to be no time savings to be had with regards either the restless or django-restless modules for django.