Search code examples
pythonflaskserver-sent-events

How to redirect when server-sent-event is finished in Flask on server side?


Question

How can one redirect to another page when server-sent-event is finished in Flask and on server side?

Problem description

An EventSource was implemented on client side code (JavaScript) and a Response was returned on server side code (Flask, Python). The EventSource will be closed if the last item from server was sent. I can't see a clear solution to redirect to another site via server side code. Maybe the answer is simple, but I'm not getting it.

Similar problem

A similar question is How to stop Server-Sent Events and one possible answer in the comment How do server-sent events actually work?. I'm not sure, if it's the same for php and python, so I've started a new question for that. I also didn't get it.

EDIT How do I close a Server-Send Events connection in Flask? is strongly related to this question. The question there is mainly to see how you can stop SSE from Flask.

Working solution on client side

So I came up with a solution that worked (a progress bar), but only on the client side. How do I have to change the code to get a working example of reconnecting via Flask functions?

Relevant code snippets

HTML/Jinja

{% block additional_stylesheets %}
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='data_import.css') }}" />
{%  endblock %}
{% block additional_javascripts %}
    <script src="{{ url_for('static', filename='js/data_import.js') }}"></script>
{% endblock %}
{% block content %}
    <div class="progress">
        <div class="progress-bar progress-bar-striped active"
             role="progressbar" aria-valuenow="0" aria-valuemin="0"
             aria-valuemax="100">
        </div>
    </div>
{% endblock %}

JavaScript

$(document).ready(function()
    {
        var source = new EventSource("/progress");

        source.addEventListener('import-progress', function(event)
            {
                $('.progress-bar')
                    .css('width', event.data + '%')
                    .attr('aria-valuenow', event.data);
            }, false
        );

        source.addEventListener('last-item', function()
            {
                source.close();
                redirect();
            }, false
        );
    }
);

# This works! But how to do the same thing without calling redirect()
# on client site?
function redirect()
{
    window.document.location.href = window.location.protocol + "//" +
            window.location.host + "/products.html";
}

Flask

from foo import app
from flask import render_template, Response

@app.route('/data_import.html')
def data_import():
    return render_template(
        'data_import.html')


@app.route('/progress')
def progress():
    return Response(import_progress(), mimetype='text/event-stream')

def import_progress():
    """
    Just a small example
    """
    for number in range(1, 101):
        sse_id = str(number)
        sse_progress = str(number)
        sse_event = 'import-progress'

        if number == 100:
            sse_event = 'last-item'

        yield "id:{_id}\nevent:{event}\ndata:{progress}\n\n".format(
            _id=sse_id, event=sse_event, progress=sse_progress)

I tried a lot to get a redirect working. But I don't know exactly how to do it. Every attempt so far has failed.


Solution

  • What you want to do is send the redirect URL as the last event - you'll still need to redirect using JavaScript, but you won't have to hard-code the path any more:

    def import_progress():
        """
        Just a small example
        """
        for number in range(1, 101):
            sse_id = str(number)
            sse_data = str(number)
            sse_event = 'import-progress'
    
            if number == 100:
                sse_event = 'last-item'
                sse_data = url_for('product_list')
    
            yield "id:{_id}\nevent:{event}\ndata:{data}\n\n".format(
                _id=sse_id, event=sse_event, data=sse_data)
    

    Then your last-item handler becomes:

    source.addEventListener('last-item', function(event) {
         source.close();
         redirect(event.data);
       }, false
    );
    

    And redirect becomes a simple:

    function redirect(url) {
      document.location = url;
    }