13

The below has given an answer using node.js.

How to close a "Server-Sent Events"-connection on the server?

However, how to do the same thing in python Flask?

Community
  • 1
  • 1
hllau
  • 9,879
  • 7
  • 30
  • 35
  • It seems the author of Flask does not have a plan to support that yet. For "Server-Sent Events", it is better to use an event-driven architecture like NodeJS. – hllau Aug 14 '12 at 04:45
  • [this comment](http://stackoverflow.com/questions/32273937/how-to-redirect-when-server-sent-event-is-finished-in-flask-on-server-side#comment52434617_32275299) was also very helpful for a similar question – colidyre Sep 18 '15 at 14:04

3 Answers3

8

It is important to note you need to close the connection on the client as well, otherwise it will try and reopen the connection after a retry timeout.

This was confusing me until I saw this post here: https://stackoverflow.com/a/38235218/5180047

From the link:

The problem here is that the server unexpectedly closes the connection, instead of doing its work while leaving it open. When this happnes, the client re-sends a request to open a connection and to start streaming Server Sent Events. The server then closes the connection again and again, leading to an infinite loop.

After I saw this.. my preferred solution was to close the connection from the server by yielding an expected data message.

On the client (browser)

var sse = new EventSource('/sse');
sse.addEventListener('message', function(e) {
  console.log(e);
  var data = e.data;
  if (!data) {
      console.log('no data in event');
      return;
  }
  if (data === 'finished') {
      console.log('closing connection')
      sse.close()
  }
  // my work here based on message
});

My Flask Server

from flask import Response

@app_api.route("/sse")
def stream():
    def trackerStream():
        finished = check_if_finished()
        while not finished:
            sleep(1) # poll timeout
            if should_send_event():
                yield f"data: {mydata()}\n\n"
            else:
                yield "data: nodata\n\n"

            finished = check_if_finished()

        yield "data: finished\n\n"

    return Response(trackerStream(), mimetype="text/event-stream")

Note: Make sure you're always sending events at some interval from the server to the client. If the user closes the browser, flask will get an error while trying to write to the socket and will close the stream for you. If you aren't writing to the client at some interval, even if you're just writing data: nodata\n\n, then the server could get stuck in a loop.

Nick Brady
  • 6,084
  • 1
  • 46
  • 71
  • 1
    This was incredibly useful! I'm ashamed to say that I had been resulting to a server-generated XSS redirect payload to return the browser to the main page after the output finished in order to avoid re-opening the connection. It was the only way I could figure to get out of the loop until I saw this answer. Very helpful. – rubynorails Jul 08 '20 at 18:16
5

Well, it depends on the architecture of your app.

Let me show you an example (see this code at https://github.com/jkbr/chat/blob/master/app.py):

def event_stream():
    pubsub = red.pubsub()
    pubsub.subscribe('chat')
    for message in pubsub.listen():
        print message
        yield 'data: %s\n\n' % message['data']

@app.route('/stream')
def stream():
    return flask.Response(event_stream(),
                          mimetype="text/event-stream")

Flask asks a new message to Redis (locking operation) steadily, but when Flask sees that streaming terminates (StopIteration, if you aren't new to Python), it returns.

def event_stream():
    pubsub = red.pubsub()
    pubsub.subscribe('chat')
    for message in pubsub.listen():
        if i_should_close_the_connection:
            break
        yield 'data: %s\n\n' % message['data']

@app.route('/stream')
def stream():
    return flask.Response(event_stream(),
                          mimetype="text/event-stream")
gioi
  • 1,463
  • 13
  • 16
  • 2
    What is the `i_should_close_the_connection`? Would you explain a bit more? – hllau Nov 02 '12 at 10:58
  • It's a boolean. You can use that `if` statement as you would do in Python. – gioi Nov 02 '12 at 13:21
  • 1
    I believe listen() is a blocking call. If nothing is published and the client disconnects, the server hangs forever. I had this problem. – David Xia Feb 19 '13 at 03:18
  • Yes, it's a blocking call. You may want to use timeouts from stdlib and close connection using `pubsub.close()`. It _should_ work but I haven't checked. – gioi Mar 02 '13 at 11:37
0

I had the same problem and finally found the following solution here.

import time
from flask import Flask, Response, stream_with_context

app = Flask(__name__)

@app.route('/stream')
def stream():
    def gen():
        try:
            i = 0
            while True:
                data = 'this is line {}'.format(i)
                print(data)
                yield data + '<br>'
                i += 1
                time.sleep(1)
        except GeneratorExit:
            print('closed')

    return Response(stream_with_context(gen()))

You can start the method with subscribing a listener to Redis. Instead of time.sleep(1) you could wait for Redis' listen() method to return a vlaue. Instead of print('closed') you could unsubscribe from Redis. The only problem left is that the GeneratorExit exception is only raised when the yield value is being sent to the client. So if Redis' listen() never ends, then you will never discover that the connection has been broken.

gogognome
  • 727
  • 8
  • 24
  • It took me a while to realize that the server may keep connections open for some time after the browser is closed. (In my case, the server only closed the connection after about 150 seconds.) Point is, it _does_ work! :) – Arel Mar 04 '20 at 06:59