Search code examples
pythonflutterfilefastapiflet

How do I return a FileResponse (or StreamingResponse) in flet-fastapi?


I'm actually trying to create a web-app that would ask a person some info, than edit \*.docx with jinja and return this \*.docx to the person as a download.

I'm having trouble making flet-fastapi return a FileResponse. My workdir looks like this:

__pycache__/
.venv/
output/
    output/test.docx
main.py
requirements.txt

requirements.txt:

flet-fastapi
flet
uvicorn
pydantic
fastapi

main.py:

from contextlib import asynccontextmanager
import flet as ft
import flet_fastapi
from fastapi import FastAPI
from fastapi.responses import FileResponse

@asynccontextmanager
async def lifespan(app=FastAPI):
    await flet_fastapi.app_manager.start()
    yield
    await flet_fastapi.app_manager.shutdown()

app = FastAPI(lifespan=lifespan)

# I believe that it doesn't matter which endpoint we choose here, since later we mount
# our flet on the root-path. Decorator is here because without it "return FileResponse()"
# does nothing at all.
@app.get("/")
async def dl_test(page: ft.Page):
    async def download(e):
        return FileResponse(f"output/test.docx", media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", filename="test.docx")
    await page.add_async(ft.OutlinedButton(text="Get file", on_click=download))
    await page.update_async()

app.mount("/", flet_fastapi.app(dl_test))

This code is the minimal reproducible code. To reproduce it, we can prepare our virtual environment:

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

And then run it:

uvicorn --reload main:app

As we run it, we can see an exception:

fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that <class 'flet_core.page.Page'> is a valid Pydantic field type. If you are using a return type annotation that is not a valid Pydantic field (e.g. Union[Response, dict, None]) you can disable generating the response model from the type annotation with the path operation decorator parameter response_model=None.

How can we make return FileResponse() working?


Solution

  • The way to do this is to use launch_url(url). Since you run your flet app as an async app (such as when running flet with fastapi), you need to use the async version of that method, i.e., launch_url_async(url) (though it might not be that clear in flet's documentation), by adding _async at the end of that method, as well as awaiting it (see the example below).

    You also need to make sure that the FastAPI endpoint—that is responsible for downloading the file—returns a FileResponse or StreamingResponse (depending on your needs—please have a look at this answer and this answer for more details and examples) with the Content-Disposition header appropriately set, in order to force downloading the file (specially, when it is in a format that usually opens in the browser, i.e., txt, pdf, mp4, etc.) instead of displaying its contents in the browser/app (please have a look at this answer, as well as this and this for more info on that).

    Working Example

    from fastapi import FastAPI
    from fastapi.responses import FileResponse
    from contextlib import asynccontextmanager
    import flet as ft
    import flet_fastapi
    
    
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        await flet_fastapi.app_manager.start()
        yield
        await flet_fastapi.app_manager.shutdown()
    
    
    app = FastAPI(lifespan=lifespan)
    
    
    @app.get('/download')
    async def download():
        headers = {'Content-Disposition': 'attachment; filename="test.txt"'}
        return FileResponse("test.txt", headers=headers)
    
    
    async def main(page: ft.Page):
        async def download_file(e):
            await page.launch_url_async(url='/download', web_window_name='_self')
            
        await page.add_async(ft.FilledButton(text="Download File", on_click=download_file))
        await page.update_async()
    
    
    # mount flet app to the root path
    app.mount('/', flet_fastapi.app(main))