39

I have a flask REST endpoint that does some cpu-intensive image processing and takes a few seconds to return. Often, this endpoint gets called, then aborted by the client. In these situations I would like to cancel processing. How can I do this in flask?

In node.js, I would do something like:

req.on('close', function(){
  //some handler
});

I was expecting flask to have something similar, or a synchronous method (request.isClosed()) that I could check at certain points during my processing and return if it's closed, but I can't find one.

I thought about sending something to test that the connection is still open, and catching the exception if it fails, but it seems Flask buffers all outputs so the exception isn't thrown until the processing completes and tries to return the result:

An established connection was aborted by the software in your host machine

How can I cancel my processing half way through if the client aborts their request?

alex
  • 479,566
  • 201
  • 878
  • 984
Crashthatch
  • 1,283
  • 2
  • 13
  • 20
  • What do you mean "aborted by the client"? Does the client attempt to close the connection? Or do they hit another endpoint? I don't think there is a way to do this using Flask, although you could certainly code your endpoints to allow for them to stop processing (using a database to keep track of these tasks and have the long-running task check that database for the stop flag, for example). – Mark Hildreth Aug 30 '13 at 04:46
  • They close the connection. I regenerate an image every time they change some options (checkboxes): if they check a new box, a request gets fired, if they immediately check a second box before the first request completes, the initial request gets cancelled (jqXHR.abort()) and a new one is made with the new options. I thought about firing an extra request from the client to write some "wascancelled" files / DB records (as you suggest), but that seems very ugly. PHP has connection_aborted(), nodejs has req.on('close'), I expected flask to have something similar. – Crashthatch Aug 30 '13 at 09:13
  • 2
    You may find [this e-mail thread](https://groups.google.com/forum/#!topic/modwsgi/jr2ayp0xesk) interesting. It discusses how a WSGI application could detect when a connection is closed. I believe it's similar to how `connection_aborted()` works in PHP: you cannot simply call it and determine if the connection was closed. Instead, you need to try to send data back first (which you might be able to pull off using [streaming](http://flask.pocoo.org/docs/patterns/streaming/)). – Mark Hildreth Aug 30 '13 at 15:43
  • I do not know Flask very well, so I can't help you determine when a client has closed the connection. However, if you can determine this, have you thought about doing the image processing in a separate process using `multiprocessing`. When a connection is closed you can call `process.terminate()` to end the process. – Michael David Watson Sep 03 '13 at 14:05

4 Answers4

22

There is a potentially... hacky solution to your problem. Flask has the ability to stream content back to the user via a generator. The hacky part would be streaming blank data as a check to see if the connection is still open and then when your content is finished the generator could produce the actual image. Your generator could check to see if processing is done and return None or "" or whatever if it's not finished.

from flask import Response

@app.route('/image')
def generate_large_image():
    def generate():
        while True:
            if not processing_finished():
                yield ""
            else:
                yield get_image()
    return Response(generate(), mimetype='image/jpeg')

I don't know what exception you'll get if the client closes the connection but I'm willing to bet its error: [Errno 32] Broken pipe

AlexLordThorsen
  • 8,057
  • 5
  • 48
  • 103
  • 1
    But where is the exception raised? How can it be captured? – Guy Sep 11 '14 at 05:08
  • 1
    The error would be generated in the socket library. It would be generated when you attempt to send data to a socket that's no longer open and a time-out occurs. – AlexLordThorsen Oct 29 '14 at 22:08
  • From this question/answer: https://stackoverflow.com/questions/31265050/how-to-make-an-exception-for-broken-pipe-errors-on-flask-when-the-client-discon . It looks like the Broken pipe exception could not be caught (or at least without your response function). – Hieu Sep 19 '17 at 07:54
  • 1
    You can catch the `GeneratorExit` exception inside the loop. Flask stops listening to your generator when the connection is closed and therefore the exception is raised. – Fabian Scheidt Jan 04 '19 at 16:14
  • 1
    I'm on Windows and this solution does not work for me. No error is produced when the client e.g. presses "Stop" in their browser. Running with `waitress.serve(app, send_bytes=1)` and yielding a nonempty string "q" instead of empty string. The client can see the single "q" characters come in streaming, but when pressing "stop" in client browser, flask app continues to think it's successfully sending "q"s. The equivalent node.js code using `req.on('close', () => {})` immediately detects browser "Stop" button press. – JustAskin Feb 26 '19 at 02:39
  • Is there any way to do this while specifying your own `headers` in the `Response` object, where the values of those headers aren't known until after `processing_finished()` completes? – Brannon Jul 28 '21 at 21:36
  • Are you able to explain what the processing_finished function and get_image function might look like? I have a simple function which returns a json and I want to implement this to cancel the request for the json if the connection is closed – James Hall May 31 '23 at 06:51
2

I was just attempting to do this same thing in a project and I found that with my stack of uWSGI and nginx that when a streaming response was interrupted on the client's end that the following errors occurred

SIGPIPE: writing to a closed pipe/socket/fd (probably the client disconnected) on request
uwsgi_response_write_body_do(): Broken pipe [core/writer.c line 404] during GET
IOError: write error

and I could just use a regular old try and except like below

    try:
        for chunk in iter(process.stdout.readline, ''):
            yield chunk
        process.wait()
    except:
        app.logger.debug('client disconnected, killing process')
        process.terminate()
        process.wait()

This gave me:

  1. Instant streaming of data using Flask's generator functionality
  2. No zombie processes on cancelled connection
Emory Petermann
  • 129
  • 1
  • 3
1

As far as I know you can't know if a connection was closed by the client during the execution because the server is not testing if the connection is open during the execution. I know that you can create your custom request_handler in your Flask application for detecting if after the request is processed the connection was "dropped".

For example:

from flask import Flask
from time import sleep
from werkzeug.serving import WSGIRequestHandler


app = Flask(__name__)


class CustomRequestHandler(WSGIRequestHandler):

    def connection_dropped(self, error, environ=None):
        print 'dropped, but it is called at the end of the execution :('


@app.route("/")
def hello():
    for i in xrange(3):
        print i
        sleep(1)
    return "Hello World!"

if __name__ == "__main__":
    app.run(debug=True, request_handler=CustomRequestHandler) 

Maybe you want to investigate a bit more and as your custom request_handler is created when a request comes you can create a thread in the __init__ that checks the status of the connection every second and when it detects that the connection is closed ( check this thread ) then stop the image processing. But I think this is a bit complicated :(.

Community
  • 1
  • 1
moliware
  • 10,160
  • 3
  • 37
  • 47
0

This question is old, but the solution suggested in this thread works for me. Flask stops listening to your generator when the connection is closed and therefore the GeneratorExit is raised. You can catch that exception in your streaming function and stop the processing:

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 = f"This is line {i}"
                print(data)
                yield data + "<br>"
                i += 1
                time.sleep(1)
        except GeneratorExit:
            print("Connection aborted.")

    return Response(stream_with_context(gen()))
Long Nguyen
  • 62
  • 1
  • 3