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?
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 await
ing 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).
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))