Catching exception in asyncio stream server

Viewed 594

Given the following program:

1    import asyncio
2    async def run():
3        try:
4            server = await asyncio.start_server(on_connected, '127.0.0.1', 15500)
5
6            async with server:
7                await server.serve_forever()
8        except:
9            print("exception!")
10 
11   async def on_connected(reader, writer):
12       while True:
13           data = await reader.readline()
14           print(1 / 0)
15   
16   asyncio.run(run())

When I run it, and use nc to connect to it & send data, it will raise an exception in line 14. However I am unable to handle it. Instead, I will get an exception printed and the program will hang.

nc test:

$ nc localhost 15500
test

program output:

$ python3 serv
Task exception was never retrieved
future: <Task finished coro=<on_connected() done, defined at serv:14> exception=ZeroDivisionError('division by zero')>
Traceback (most recent call last):
  File "serv", line 14, in on_connected
    print(1 / 0)
ZeroDivisionError: division by zero

While I of course could just add a try/except around line 14, I want to implement a general exception handling which shall handle all errors which can ever occur inside of on_connected.

How can I do this?

2 Answers

Asyncio provides a method to set a general exception handler

Let's apply it to this example:

  • Let's just define our loop exception handler: handle_exception
  • And inside this method let's manage exceptions. Next code just customize logging messages.
import asyncio
import logging

log = logging.getLogger(__name__)


async def run():
    loop = asyncio.get_running_loop()
    loop.set_exception_handler(handle_exception)
    server = await asyncio.start_server(on_connected, '127.0.0.1', 5062)

    async with server:
        await server.serve_forever()

async def on_connected(reader, writer):
    while True:
        data = await reader.readline()
        raise print(1 / 0)

def handle_exception(loop, context):
    # context["message"] will always be there; but context["exception"] may not
    msg = context.get("exception", context["message"])
    if name := context.get("future").get_coro().__name__ == "on_connected":
        if type(msg) == ZeroDivisionError:
            log.error(f"Caught ZeroDivisionError from on_connected: {msg}")
            return
        log.info(f"Caught another minor exception from on_connected: {msg}")
    else:
        log.error(f"Caught exception from {name}: {msg}")

Output on this case is the following:

Caught ZeroDivisionError from on_connected: division by zero

Reference that I have used to look for further information:

Please, let me know if this answer your question.

When developing server code, bug causing an exception often occurs and then the server process just hangs and must be killed, which is super anoying.

I wanted it to exit cleanly as soon as one exception is raised in the protocol code. To do so, I created a handler class that closes the server and registered it as the exception handler. The call to await server.serve_forever() will throw CancelledError, so you may want to catch it to exit cleanly.

import asyncio


class ExcHandler:
    def __init__(self, server):
        self._server = server

    def __call__(self, loop, context):
        print("exception occured, closing server")
        loop.default_exception_handler(context)
        self._server.close()

class EchoServerProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        raise RuntimeError("oups")


async def main():
    
    loop = asyncio.get_running_loop()

    server = await loop.create_server(
        lambda: EchoServerProtocol(),
        '127.0.0.1', 8888)

    loop.set_exception_handler(ExcHandler(server))

    async with server:
        try:
            await server.serve_forever()
        except asyncio.exceptions.CancelledError:
            print("server cancelled")

asyncio.run(main())

Apparently, the server catching exceptions is by design (See Python issue #42526). The rationale behind that is the server keeps processing other requests, even when one of them crash. I guess this is suitable for production where the show must go on even in case of glitches.

Related