For things like server sent events, it would be really great to have an easy option to check if the client connection of a streamed request is still open.
Something like this:
from flask import Flask, Response, client, stream_with_context
app = Flask('sse')
@app.route('/stream')
def sse():
def gen_sse():
with open('log.txt') as f:
while client.is_connected():
lines = f.read()
if lines:
yield build_sse(lines)
time.sleep(1)
return Response(
stream_with_context(gen_sse()),
mimetype='text/event-stream',
)
def build_sse(message, id_=None):
sse = ''
if id_ is not None:
sse += f'id: {id_}\n'
sse += 'data: ' + '\ndata:'.join(message.splitlines())
sse += '\n\n'
return sse
Does the WSGI protocol even expose this information?
Maybe this helps? https://github.com/bottlepy/bottle/issues/414
I tried using
class SSEGenerator:
def __init__(self):
self.closed = False
def close(self):
print('closed')
self.closed = True
def __iter__(self):
with open('log.txt') as f:
i = 0
while not self.closed:
lines = f.read()
if lines:
yield build_sse(lines, id_=i)
time.sleep(1)
@app.route('/stream')
def sse():
return Response(
SSEGenerator(),
mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache'}
)
But the close method of the generator is not called on client disconnect.
@MaxNoe My technique for detecting gone clients in streaming handlers is to watch the I/O operations that you do in your handler for errors. If you write to a socket that has been closed from the other side, the operation will raise an OSError, typically with an errno value of EPIPE, or sometimes EBADF.
Isn't that sufficient for your needs? I don't think there is a way to detect a closed socket without writing or reading on it.
In the example i've given, I don't get any kind of exception
Well, that is not what I see here. I adapted your example code to a standalone app as follows:
import time
from flask import Flask, Response, stream_with_context
app = Flask(__name__)
@app.route('/stream')
def stream():
def gen():
i = 0
while True:
data = 'this is line {}'.format(i)
print(data)
yield data + '<br>'
i += 1
time.sleep(1)
return Response(stream_with_context(gen()))
I connect to http://localhost:5000/stream and the lines start coming, both on the browser and on the console. If I close the browser, a second or two later the lines stop printing in the console, which means that Flask caught the OSError exception and cancelled the generator.
As I said above, the closed socket is detected when a write is done on the socket. Flask is writing the lines that you yield, and as far as I can see it is handling the closing of the socket in the correct way.
One more thing. If what you are asking is to have a way to be notified when the generator is stopped, then you can catch GeneratorExit in your function. Here is my above example modified to be notified when the client went away:
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()))
I think I understand now. I try streaming a log file. So as long as not a new line from the logfile comes in, my generator does not try to stream a new result.
I see. So your generator must be blocking on the f.read() line for a while. Unfortunately there is nothing you can do to escape that blocking call when the client goes away. I would ensure your log file gets updated often, maybe a heartbeat log line that prevents your generator from ever blocking for too long on that read call.
The f.read does not block, it immediately returns an empty string. I could send an empty sse.
Ok, i moved the check for the emptiness from the server to the client (basically sending an empty sse every second) and it works nicely now.
However, if there is a way comparable to connection_status in php, that would be really helpful.
@MaxNoe read the first user note in the connection_status documentation. That matches what I told you above, without writing on the connection php is also unable to detect a closed socket. The difference is that php appears to record the closed state in this connection_status variable so that the application can check it, while Flask closes the generator. Add a try/except for GeneratorExit as I showed you and you will have a notification on socket closed.
Thanks for the help and explanations!
Most helpful comment
Well, that is not what I see here. I adapted your example code to a standalone app as follows:
I connect to
http://localhost:5000/streamand the lines start coming, both on the browser and on the console. If I close the browser, a second or two later the lines stop printing in the console, which means that Flask caught theOSErrorexception and cancelled the generator.As I said above, the closed socket is detected when a write is done on the socket. Flask is writing the lines that you yield, and as far as I can see it is handling the closing of the socket in the correct way.