4

I want to create a waiting page that I can render this page before executing some long function. And when it finishes, it will redirect to success page in return. This is an simple example of code

@app.route("/process", methods=['GET', 'POST'])
def process():
    with app.app_context():
        data = render_template('waiting.html')

    ### .. Do something long and have to wait .. ###
    # e.g. sleep a few seconds

    return redirect(url_for('success'))

What's happened is that the line data = render_template('waiting.html') seems not executed. What I see is blank screen for a while (as it is processing function below). And, when it finishes, it shows success page as expected.

It seems to be a simple task that many questions have been asked here before. But those solutions are not working with me. My goal is just to make the template show up before doing a task. So that users will see that their requests are in progress.

Here are what I've tried.

Please kindly tell me if I misunderstand the concept of Flask. Thanks!

Fony Lew
  • 505
  • 4
  • 16

2 Answers2

5

There are two things that you are missing here:

  1. what render_template actually does
  2. how the HTTP flow works

For 1 the answer is quite simple. render_template will take the path to the template you want to render and the context that you want to render the template with and return a string. So it will not do anything to the request, it will just generate a string based on the template and the context (and in your code sample, it will store this rendered string in the data variable).

For 2 what you need to understand is that when an user makes a request to your /process route it will expect a response, and only one response.

So your route needs to decide what to respond: will it be a 200 OK with some content (some HTML for example)? or a 301 Redirect that will instruct the user's browser to go to another page? You can't have both responses as it violates the protocol.

In order to achieve the experience you want you will have to instruct the user's browser to make multiple requests to your server. There are a few strategies you can use for this, but the simplest one is probably to use Ajax.

The idea is that your initial route will just render the waiting.html page, but with a twist. In the page you will add some JavaScript code that makes an Ajax request to a different route that will do the actual long running job.

So now you have two routes:

@app.route("/process", methods=['GET', 'POST'])
def process():
    if request.method == 'GET':
        return render_template('waiting.html')

    if request.method == 'POST':

        ### .. Do something long and have to wait .. ###
        # e.g. sleep a few seconds

        return 'done'

(even though it's only one method, they are essentially two routes: GET /process and POST /process)

The JavaScript in your page will look something like this:

var request = new XMLHttpRequest();
request.open('POST', '/process');

request.onload = function() {
  if (request.status === 200 && request.responseText === 'done') {
    // long process finished successfully, redirect user
    window.location = '/success';
  } else {
    // ops, we got an error from the server
    alert('Something went wrong.');
  }
};

request.onerror = function() {
  // ops, we got an error trying to talk to the server
  alert('Something went wrong.');
};

request.send();

(or if you are using jQuery)

What this will do is it will initiate another HTTP request to your server in the background that will kick-off that long running job and will receive as response the text done. When this happens, it will redirect the user to the /success page.

Now, it's important to keep in mind that this is a very simple and you need to keep somethings in mind:

  1. each one of these Ajax requests will block a connection to your server, which, if you are using something like gunicorn, would be a full worker. So if your processing takes too long or if you have a lot of concurrent users accessing this route your server will be very overloaded.

  2. you need to handle errors properly. In my example I always return done with a 200 OK (which is the default status code), but if something goes wrong in the processing you would return something different and possibly some kind of error message to show to the user.

Luiz Aoqui
  • 271
  • 4
  • 13
  • Sorry for late reply and thank you for your clear explanation. I did misunderstand about render_template. Now I get your point about HTTP response. Actually I've tried your answer since that day. But I had to modify my code in other part a little bit to make this working. I will let you know the result! – Fony Lew Sep 10 '17 at 14:40
  • Your code do a trick! After I modified the requests and javascript. It works like a charm. I really appreciate your explanation. Thank you again! – Fony Lew Sep 11 '17 at 10:26
  • I would like to ask another question. If I want to update progress to template e.g. subtask 2/5. Would you please suggest me how should I do? – Fony Lew Nov 02 '17 at 11:57
  • 1
    This will be hard to answer in a comment, so I would suggest you create a new question. But again you have a few options, I would suggest you Google for some techniques like Websockets (might an overkill), Server sent events (SSE) (didn't catch on, so not very popular), polling and long polling (requires you to keep a state in your server). – Luiz Aoqui Nov 14 '17 at 15:43
1

Waiting data is not returned. Try this solution using yield based on https://stackoverflow.com/a/31831773/3054766 mentioned

from flask import Response, stream_with_context

@app.route("/process", methods=['GET', 'POST'])
def process():
    def generate_output(): 
        with app.app_context():
            yield render_template('waiting.html')

        ### .. Do something long and have to wait .. ###
        # e.g. sleep a few seconds

        # Later to find that we Can't yield redirect
        # yield redirect(url_for('success'))

    return Response(stream_with_context(generate_output()))

Update: sorry to find that yield can only be used to stream data to one page but not for redirect.

leaf
  • 1,624
  • 11
  • 16
  • Amazing, I finally saw my waiting page. Though, I got `AssertionError: applications must write bytes` at the end. I'm not sure why. – Fony Lew Sep 08 '17 at 09:13
  • Thank you for your kind response. Good to know about this anyway :) – Fony Lew Sep 08 '17 at 09:45
  • 1
    Maybe it could work with a litte bit of javascript https://stackoverflow.com/questions/1435015/how-can-i-make-the-browser-wait-to-display-the-page-until-its-fully-loaded/1435087#1435087 – VPfB Sep 08 '17 at 18:38
  • `yield` is useful when you need to buffer your response. It won't allow you to make two responses. – Luiz Aoqui Sep 11 '17 at 20:27