How to detect client disconnection to sanic server

con = db_pool.acquire()
#client cancels request at some point however server continues to the end
con.execute()
con.release()

How to detect the connection termination from client and spare server any pointless resource occupation and release them as soon as possible.

1 Like

Do you mean a premature disconnection of a request, or closing of a websocket or stream response?

I’m not sure which one I mean nor even if I’m mistaken from very beginning but
imagine a client issues a http request and if not receive response within 3 sec., times out.
On the server side it takes more than 3 sec. to provide for that request but since no one expecting it no need to go on.

Got it.

So, it somewhat depends what kind of handler you have.

As you may know, Sanic allows your handler to be either a regular function:

def my_request(request):
    ...

or async:

async def my_request(request):
    ...

If your view function is async , then you really do not need to do anything. The Task will be cancelled for you. If it is a regular function, then (unfortunately), it still will continue to execute because it does not know that the connection was closed.

To illustrate this, run the following in a terminal. In a second, try to curl the /async and the /sync endpoints. Before they return, hit Ctrl+C to stop the request. You should see that the /async will just stop. That is because the task has been cancelled. If you spawned a new task from inside there your request, it will not be stopped. In the /sync example, it will continue to run until completion.

import asyncio
import time
from sanic import Sanic, response


app = Sanic()

@app.get("/async")
async def do_slowly_async(request):
    print("Ready to sleep")
    for _ in range(5):
        print("\t ZZzzz...")
        await asyncio.sleep(1)
    print("Done sleeping")
    return response.json({"yawn": True})

@app.get("/sync")
def do_slowly_sync(request):
    print("Ready to sleep")
    for _ in range(5):
        print("\t ZZzzz...")
        time.sleep(1)
    print("Done sleeping")
    return response.json({"yawn": True})


app.run(debug=True)

With that said, it does give me some thought that perhaps the Sanic server could provide a hook, or at least the option of a hook to provide a way to do some sort of cleanup in those cases. :thinking:

Let me know if this helps.

2 Likes

Considering the provided code by @ahopkins, there is an explicit risk of resource leakage under spotlight.

Assume the following Sanic route:

@app.get("/async")
async def do_something(request):
db_connection = await connect_to_db()
#A_POINT
some_result = await run_a_query_which_takes_long_time(db_connection)
await release_connection(db_connection)
return text(some_result)

If a client aborts his connection (by closing socket) while the code is in A_POINT, his connection to the database would not be released. The worse is yet to come. A malicious user may try to exploit the vulnerability by draining the connections in the database pool and stop the program from providing service.

I think we can handle the problem by adding another middleware (beside “response” and “request”) which would be called whenever a user aborts the request he has made.

I believe Middleware should still run. Will test it out.

When a user aborts an upload to my application, I need to perform some actions. I was wondering if there are any updates and/or examples of how to handle this case?

I was trying to play around with this a little bit. The answer will slightly depend on what server you are using.

Here is a working example that I tested with uvicorn and hypercorn.

from sanic import Sanic
from sanic.response import text

app = Sanic()


@app.post("/", stream=True)
async def post(request):
    print("Started", flush=True)
    expected = int(request.headers.get("content-length"))
    result = ""
    while True:
        body = await request.stream.read()
        if body is None:
            break
        result += body.decode("utf-8")
    if expected > len(result):
        print("aborted upload", flush=True)
    else:
        print("Upload done", flush=True)
    return text("Done.")