Reputation: 91
I have an endpoint, which triggers a process in FastAPI. This process takes several minutes to complete and produces a CSV file that can later be downloaded by users.
How can I return a message saying that the process is running and redirect the users to the "Download" page, after the process has finished?
So far, I am returning an HTML response pointing to the "Download" page, but if the user clicks on it before the process has finished, they may not get the correct results:
@router.post("/run")
async def run(background_tasks: BackgroundTasks) -> HTMLResponse:
"""
1. Run model.
2. Retrieve output file.
"""
background_tasks.add_task(optimization_process)
content = """
<label for="file">Generating results:</label><br>
<progress id="file" max="100" value="70"> 70% </progress><br>
<body> Visit <a href="./get-results">result page</a> to get the results
</body>
"""
return HTMLResponse(content=content)
I am looking to improve this setup by informing the users that the process has started and redirecting them to the next screen, after the successful run of optimization_process
.
Upvotes: 5
Views: 593
Reputation: 34560
First, I would suggest having a look at this answer and this answer, in order to better understand how FastAPI deals with normal def
compared to async def
endpoints and/or background tasks, since the optimization_process()
function you are using as a background task (even though you haven't provided that in your question) suggests that you might be performing a blocking CPU-bound operation inside. Hence, a background task might not be the best choice, or you might not be using it in the proper way (e.g., you might be running a blocking operation inside that would block the event loop, whereas it should instead be running in a separate thread or process—more details and examples are given in the linked answers above).
As for redirecting the user to the "Download" page after the processing is done, you mgiht want to implement one of the solutions provided below.
One solution would be to immediately respond to the user (after /run
route is called) with a unique id
, which they could use to check on the status of their request (i.e., whether the processing of their request is still pending or done), similar to this answer and Option 2 of this answer. Once the processing is completed, the user could then move on to calling the /download
route with the given id
, in order to download the results.
This solution follows the same concept as the previous one, but instead of having the user checking on the status and redirecting themselves to the /download
page, one could automate this process by having a JavaScript function to periodically check on the status of the request and redirect the user to the "Download" page automatically, once the processing is completed.
Note that for security/privacy reasons, as outlined in Solution 1 of this answer (see that for more details), in the example below, the id
is passed to the request body instead of the query string. Hence, that is the reason for using a POST request method instead of GET, when trying to check on the status of the request, as well as when retrieving/downloading the results. If that's not important in your case scenario, as well as you are using the HTTPS protocol and have an authentication system in place that would prevent unauthorized users from accessing sensitive information (even when they have a task id
in their hands), you could instead use a GET request and pass the id
to the URL as a query or path parameter.
As for the results, if you would like to include a "Download" file link in the download.html
page for the user to click on and download some file instead, please have a look at the second linked answer in Solution 1 above on how to achieve that. You could also have your FastAPI application return a RedirectResponse
(see this answer and this answer on how to redirect from a POST to a GET route) instead of a Jinja2 template when the /download
endpoint is called, which could lead to another endpoint returning a FileResponse
, similar to the one in the second linked answer in Solution 1 above. In this way, the file downloading process would be triggered automatically, once the request processing is completed, without having the user to click on a link in the "Download" page first—it all depends on one's needs and requirements for which approach to choose.
app.py
from fastapi import FastAPI, Request, Form, BackgroundTasks
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
import time
import uuid
app = FastAPI()
templates = Jinja2Templates(directory="templates")
fake_db = {}
class Item:
def __init__(self, status):
self.status = status
self.results = ""
def process_request(id):
time.sleep(5)
fake_db[id].status = "Done"
fake_db[id].results = "This is sample data"
@app.get('/')
async def main(request: Request):
return templates.TemplateResponse(request=request, name="index.html")
@app.post('/run')
async def run(request: Request, background_tasks: BackgroundTasks):
# create a unique id for this request
id = str(uuid.uuid4())
# do some processing after returning the response
background_tasks.add_task(process_request, id)
fake_db[id] = Item("Pending")
return {"id": id}
@app.post("/status")
async def check_status(id: str = Form()):
if id in fake_db:
return {'status': fake_db[id].status}
else:
return JSONResponse("ID Not Found", status_code=404)
@app.post('/download')
async def download(request: Request, id: str = Form()):
# use the id to retrieve the request status and results
if id in fake_db and fake_db[id].status == "Done":
# Return some results. See the 2nd link in Solution 1 on how to include a "Download" file link instead
context = {"results": fake_db[id].results}
return templates.TemplateResponse(request=request, name="download.html", context=context)
else:
return JSONResponse("ID Not Found", status_code=404)
templates/index.html
<!DOCTYPE html>
<html>
<body>
<input type="button" value="Start processing" onclick="start()">
<div id="msg" href=""></div>
<script type="text/javascript">
function start() {
fetch('/run', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
let msg = "Please wait while your request is being processed.\
You will be redirected to the next screen, once your request is completed.";
document.getElementById("msg").innerHTML = msg;
intervalID = setInterval(() => {
checkStatus(data.id, intervalID);
}, 2000);
})
.catch(error => {
console.error(error);
});
}
function checkStatus(requestID, intervalID) {
var data = new FormData();
data.append("id", requestID)
fetch('/status', {
method: 'POST',
body: data,
})
.then(response => response.json())
.then(data => {
if (data.status == "Done") {
clearInterval(intervalID);
redirect("/download", {
id: requestID
});
}
})
.catch(error => {
console.error(error);
});
}
function redirect(path, params, method='post') {
const form = document.createElement('form');
form.method = method;
form.action = path;
for (const key in params) {
if (params.hasOwnProperty(key)) {
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = key;
hiddenField.value = params[key];
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
</script>
</body>
</html>
templates/download.html
<!DOCTYPE html>
<html>
<body>
<h1> Download Results </h1>
{{ results }}
</body>
</html>
Upvotes: 5