Search code examples
pythonfilefastapimultipartform-datastarlette

How to get Files and Form data using the Request object in FastAPI?


I am developing a webhook in which a third-party service will hit my URL and will provide some files, now I can not use FastAPI's UploadFile = File (...) because it throws an error of the required field File I want to read the payload and files from the request object as we can do in Flask by simply doing this

from flask import request
files = request.files

How can I achieve the same in FastAPI?


Solution

  • The proper approach would be to define File/UploadFile and Form type parameters in your endpoint, as demonstrated in Method 1 of this answer, as well as here, here and here (for a faster file-upload approach, see this and this as well). For instance:

    @app.post("/submit")
    async def register(name: str = Form(...), files: List[UploadFile] = File(...)):
        pass
    

    However, since you are looking for an approach using the Request object, you could use FastAPI/Starlette's await request.form() method to parse the body, which would return a FormData object, containing all the File(s) and Form data submitted by the user.

    Simple Working Example

    from fastapi import FastAPI, Request
    
    app = FastAPI()
    
    
    @app.post("/submit")
    async def submit(request: Request):
        return await request.form()
    

    A more complete example is given below, which also demonstrates how to obtain every File and Form input in the FormData object returned by Starlette. For more details and references, please have a look at this answer, on which the following example is based. Another solution would be to use the approach demonstrated in Option 1 or Option 2 of this answer. Also, if, for any reason, you would like to use a def instead of async def endpoint, please have a look at this answer on how to read the file contents inside a def endpoint. You might find this answer helpful as well.

    Complete Working Example

    app.py

    from fastapi import FastAPI, Request, Depends, HTTPException
    from fastapi.responses import HTMLResponse
    from fastapi.templating import Jinja2Templates
    from starlette.datastructures import FormData, UploadFile
    
    app = FastAPI()
    templates = Jinja2Templates(directory='templates')
    
    
    async def get_body(request: Request):
        content_type = request.headers.get('Content-Type')
        if content_type is None:
            raise HTTPException(status_code=400, detail='No Content-Type provided!')
        elif (content_type == 'application/x-www-form-urlencoded' or
              content_type.startswith('multipart/form-data')):
            try:
                return await request.form()
            except Exception:
                raise HTTPException(status_code=400, detail='Invalid Form data')
        else:
            raise HTTPException(status_code=400, detail='Content-Type not supported!')
    
    
    # Use this approach, if keys (names) of Form/File data are unknown to the backend
    @app.post('/submit')
    async def submit(body=Depends(get_body)):
        if isinstance(body, FormData):  # if Form/File data received
            for k in body:
                entries = body.getlist(k)
                if isinstance(body.getlist(k)[0], UploadFile): # check if it is an UploadFile object
                    for file in entries:
                        print(f'Filename: {file.filename}. Content (first 15 bytes): {await file.read(15)}')
                else:
                    data = entries if len(entries) > 1 else entries[0]
                    print(f"{k}={data}")
    
            return 'OK'
    
    
    # Use this approach, if keys (names) of Form/File data are known to the backend beforehand
    @app.post('/other')
    async def other(body=Depends(get_body)):
        if isinstance(body, FormData):  # if Form/File data received
            items = body.getlist('items')
            print(f"items={items}")
            msg = body.get('msg')
            print(f"msg={msg}")
            files = body.getlist('files')  # returns a list of UploadFile objects
            if files:
                for file in files:
                    print(f'Filename: {file.filename}. Content (first 15 bytes): {await file.read(15)}')
                    
            return 'OK'
    
    
    @app.get('/', response_class=HTMLResponse)
    async def main(request: Request):
        return templates.TemplateResponse('index.html', {'request': request})
    

    Test using HTML <form>

    Please make sure that each <input> element, for both form and file inputs, includes a name attribute, as shown below; otherwise, it won't be included in Starlette's FormData object.

    templates/index.html

    <!DOCTYPE html>
    <html>
       <body>
          <form method="post" action="/submit"  enctype="multipart/form-data">
             msg : <input type="text" name="msg" value="test"><br>
             item 2 : <input type="text" name="items" value="1"><br>
             item 2 : <input type="text" name="items" value="2"><br>    
             <label for="fileInput">Choose file(s) to upload</label>
             <input type="file" id="fileInput" name="files" multiple>
             <input type="submit" value="submit">
          </form>
       </body>
    </html>
    

    Test using JavaScript's Fetch API

    templates/index.html

    <!DOCTYPE html>
    <html>
       <body>
          <form id="myForm" >
             msg : <input type="text" name="msg" value="test"><br>
             item 1 : <input type="text" name="items" value="1"><br>
             item 2 : <input type="text" name="items" value="2"><br>
          </form>
          <label for="fileInput">Choose file(s) to upload</label>
          <input type="file" id="fileInput" name="files" multiple><br>
          <input type="button" value="Submit" onclick="submitUsingFetch()">
          <p id="resp"></p>
          <script>
             function submitUsingFetch() {
                const resp = document.getElementById("resp");
                const fileInput = document.getElementById('fileInput');
                const myForm = document.getElementById('myForm');
                var formData = new FormData(myForm);
                for (const file of fileInput.files)
                   formData.append('files', file);
             
                fetch('/submit', {
                      method: 'POST',
                      body: formData,
                   })
                   .then(response => response.json())
                   .then(data => {
                      resp.innerHTML = JSON.stringify(data); // data is a JSON object
                   })
                   .catch(error => {
                      console.error(error);
                   });
             }
          </script>
       </body>
    </html>
    

    Test using Python requests

    test.py

    import requests
    
    url = 'http://127.0.0.1:8000/submit'
    data = {'items': ['foo', 'bar'], 'msg': 'Hello!'}
    files = [('files', open('a.txt', 'rb')), ('files', open('b.txt', 'rb'))]
     
    # Send Form data and files
    r = requests.post(url, data=data, files=files)  
    print(r.text)
    
    # Send Form data only
    r = requests.post(url, data=data)              
    print(r.text)