filedownloadfastapiresponsestarlette

FastAPI - How to specify filename when downloading bytes content using Response class?


I need to download files stored in MongoDB (and not in file system), using FastAPI. I use FastAPI's Response class for this, because FileResponse can only be used if files are in the file system. Downloading works, but I cannot find a way to specify the filename when downloading the file.

In Swagger, at least, the filename is by default response_SOME-NUMBER.txt.

Here's the code:

@app.get("/files/{file_name}", responses = {200: {"content": {"text/html": {}}}}, response_class = Response)
def download_file(file_name: str):
    with MongoClient() as client:
        files_collection = client[DB][FILES_COLLECTION]
        result = files_collection.find_one({"file_name": file_name})
        content = result["file_data"]

    if result is not None:
        return Response(content=content, media_type="text/html")
    else:
        raise HTTPException(status_code=404, detail="File not found")

I guess, I can add the filename in the headers to be retrieved by the UI, like this:

headers = {'Content-Disposition': f'attachment; filename="{download_file_name}"'}
return Response(content=content, headers=headers, media_type="text/html")

But I would like to know if there is a way to specify the name, similarly to FileResponse.


Solution

  • As it has been previously described in the following answers:

    1. How to upload a csv file using Jinja2 Templates and FastAPI , and return it after modifications?

    2. How to return a PDF file from in-memory buffer using FastAPI?

    3. How to return and download Excel file using FastAPI?

    4. Download PDF file using pdfkit and FastAPI

    5. How do I download a file from FastAPI backend using JavaScript Fetch API in the frontend?

    6. How to generate and return a PDF file from in-memory buffer using FastAPI?

    7. How to Download a File after POSTing data using FastAPI?

    8. How to have the dialog box for choosing download location appeared in the frontend, before the file gets downloaded, using FastAPI?

    9. How to display a Matplotlib chart with FastAPI/ Nextjs without saving the chart locally?

    10. Render NumPy array in FastAPI

    when returning a Response instance, one could set the Content-Disposition response header, indicating whether a file should be displayed inside the browser/webpage (using inline as the value for the first parameter in the header, which is the default) or downloaded (using attachment), as well as specify the filename (which is optional) when the file is downloaded to the client's device.

    Note that "the string following filename should always be put into quotes; but, for compatibility reasons, many browsers try to parse unquoted names that contain spaces" (if the filename contains unicode characters, one should instead use the filename* parameter and encode the filename value—see this answer).

    Example

    from FastAPI import Response
    ...
    
    # use this, if you would like to have the file displayed inside the browser instead
    # headers = {'Content-Disposition': 'inline; filename="out.html"'}
    
    headers = {'Content-Disposition': 'attachment; filename="out.html"'}
    return Response(content=content, headers=headers, media_type='text/html')
    

    When returning a FileResponse—which inherits from Response, as every other response class in FastAPI/Starlette—and setting the filename parameter, FastAPI/Starlette, behind the scenes, actually sets the Content-Disposition header for you. This can be seen in the implementation of FileResponse class here:

    class FileResponse(Response):
        chunk_size = 64 * 1024
    
        def __init__(
            self,
            ...
            headers: typing.Mapping[str, str] | None = None,
            filename: str | None = None,
            content_disposition_type: str = "attachment",
        ) -> None:
            ...
            self.filename = filename
            ...
            self.init_headers(headers)
            if self.filename is not None:
                content_disposition_filename = quote(self.filename)
                if content_disposition_filename != self.filename:
                    content_disposition = "{}; filename*=utf-8''{}".format(
                        content_disposition_type, content_disposition_filename
                    )
                else:
                    content_disposition = '{}; filename="{}"'.format(
                        content_disposition_type, self.filename
                    )
                self.headers.setdefault("content-disposition", content_disposition)
    

    As explained earlier, and as shown in the implementation of FileResponse above, Starlette will check if the filename value passed contains non-ascii/unicode characters, and if so, it will use the filename* parameter instead and send the value encoded.

    Hence, in your case, you could either set the Content-Disposition header on your own (which might be the most suitable solution), as demonstrated in the example earlier, or implement your own custom response class, inheriting from Starlette's Response, which would accept bytes content and a filename parameter for setting the header as in FileResponse above.