How to Download a File after POSTing data using FastAPI?

Use the Form keyword to define Form-data in your endpoint, and more specifically, use Form(...) to make a parameter required, instead of using await request.form() and manually checking if the user submitted the required parameters. After processing the received data and generating the audio file, you can use FileResponse to return the file to the user. Note: use the headers argument in FileResponse to set the Content-Disposition header using the attachment parameter—as described in this answer—to have the file downloaded to your device. Failing to set the headers, or using the inline parameter isntead, would lead to 405 Method Not Allowed error, as the browser attempts to access the file using a GET request (however, only POST requests are allowed to the /text2speech endpoint). Have a look at Option 1 in the examples below.

If you wanted the /text2speech endpoint supporting both GET and POST requests (as shown in your question), you could either use @app.api_route("/text2speech", methods=["GET", "POST"]) and use request.method to check which one has been called, or define two different endpoints e.g., @app.post('/text2speech') and @app.get('/text2speech'). However, you don’t necessarily need to do that in this case. Additionally, you have added a Download hyperlink to your template for the user to download the file. However, you haven’t provided any information as to how you expect this to work. This wouldn’t work in a scenario where you don’t have static files, but dynamically generated audio files (as in your case), as well as multiple users accessing the API at the same time; unless, for example, you generated random UUIDs for the filenames and saved the files in a StaticFiles directory—or added that unique identifier as a query/path parameter (you could also use cookies instead, see here and here) to the URL in order to identify the file to be downloaded—and sent the URL back to the user. In that case, you would need a Javascript interface/library, such as Fetch API, to make an asynchronous HTTP request—as described in this answer—in order to get the URL to the file and display it in the Download hyperlink. Have a look at Option 2 below. Note: The example in Option 2 uses a simple dict to map the filepaths to UUIDs, for demo purposes. In a real-world scenario, where multiple users access the API and several workers might be used, you may consider using a database storage, or Key-Value stores (Caches), as described here and here. You would also need to have a mechanism for deleting the files from the database and disk, once they have been downloaded, as well as make sure that users do not have unauthorised access to other users’ audio files.

Option 1

app.py

from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import os

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("https://stackoverflow.com/")
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

@app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
    # do some processing here
    filepath="./temp/welcome.mp3"
    filename = os.path.basename(filepath)
    headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
    return FileResponse(filepath, headers=headers, media_type="audio/mp3")

An alternative to the above would be to read the file data inside your endpoint (or if the data were already fully loaded into memory, such as here, here and here) and return a custom Response directly, as shown below:

from fastapi import Response

@app.post('/text2speech')
   ...
    with open(filepath, "rb") as f:
        contents = f.read()  # contents could be already loaded into RAM
    
    headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
    return Response(contents, headers=headers, media_type="audio/mp3")

In case you had to return a file that is too large to fit into memory—e.g., if you have 8GB of RAM, you can’t load a 50GB file—you could use StreamingResponse, which would load the file into memory in chunks and process the data one chunk at a time (If you find yield from f being rather slow, have a look at this answer):

from fastapi.responses import StreamingResponse

@app.post('/text2speech')
    ...
    def iterfile():
        with open(filepath, "rb") as f:
            yield from f

    headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
    return StreamingResponse(iterfile(), headers=headers, media_type="audio/mp3")

templates/index.html

<!DOCTYPE html>
<html>
   <head>
      <title>Convert Text to Speech</title>
   </head>
   <body>
      <form method="post" action="http://127.0.0.1:8000/text2speech">
         message : <input type="text" name="message" value="This is a sample message"><br>
         language : <input type="text" name="language" value="en"><br>
         <input type="submit" value="submit">
      </form>
   </body>
</html>

Option 2

app.py

from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import uuid
import os

app = FastAPI()
templates = Jinja2Templates(directory="templates")

files = {}

@app.get("https://stackoverflow.com/")
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

@app.get('/download')
def download_file(request: Request, fileId: str):
    filepath = files.get(fileId)
    if filepath:
        filename = os.path.basename(filepath)
        headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
        return FileResponse(filepath, headers=headers, media_type="audio/mp3")    
    
@app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
    # do some processing here
    filepath="./temp/welcome.mp3"
    file_id = str(uuid.uuid4())
    files[file_id] = filepath
    file_url = f'/download?fileId={file_id}'
    return {"fileURL": file_url}

templates/index.html

<!DOCTYPE html>
<html>
   <head>
      <title>Convert Text to Speech</title>
   </head>
   <body>
      <form method="post" id="myForm">
         message : <input type="text" name="message" value="This is a sample message"><br>
         language : <input type="text" name="language" value="en"><br>
         <input type="button" value="Submit" onclick="submitForm()">
      </form>

      <a id="downloadLink" href=""></a>

      <script type="text/javascript">
         function submitForm() {
             var formElement = document.getElementById('myForm');
             var data = new FormData(formElement);
             fetch('/text2speech', {
                   method: 'POST',
                   body: data,
                 })
                 .then(response => response.json())
                 .then(data => {
                   document.getElementById("downloadLink").href = data.fileURL;
                   document.getElementById("downloadLink").innerHTML = "Download";
                 })
                 .catch(error => {
                   console.error(error);
                 });
         }
      </script>
   </body>
</html>

Related answers to the option above can also be found here, as well as here and here.

Removing a File after it has been downloaded

To remove a file after it has been downloaded by the user, you can simply define a BackgroundTask to be run after returning the response. For example, for Option 1 above:

from fastapi import BackgroundTasks
import os

@app.post('/text2speech')
def convert(request: Request, background_tasks: BackgroundTasks, ...):
    filepath="welcome.mp3"
    # ...
    background_tasks.add_task(os.remove, path=filepath)
    return FileResponse(filepath, headers=headers, media_type="audio/mp3")

For Option 2, however, you would have to make sure to delete the key (i.e., file_id) pointing to the given filepath from the cache as well. Hence, you should create a task function, as shown below:

from fastapi import BackgroundTasks
import os

files = {}

def remove_file(filepath, fileId):
    os.remove(filepath)
    del files[fileId]
       
@app.get('/download')
def download_file(request: Request, fileId: str, background_tasks: BackgroundTasks):
    filepath = files.get(fileId)
    if filepath:
        # ...
        background_tasks.add_task(remove_file, filepath=filepath, fileId=fileId)
        return FileResponse(filepath, headers=headers, media_type="audio/mp3")    

Leave a Comment