pythonpython-asyncioquart

wrapping requests library request in Quart run_sync causes intermittent failures


I'm converting a Flask app to Quart and trying not to change too much, so for now I'm making requests on the server using the requests library, and just wrapping them in run_sync. So I converted:

            response: Response = session.request(
                method,
                url,
                params=params,
                data=data,
                headers=headers,
                json=json,
                files=files,
                cookies=cookies,
                timeout=(connect_timeout, timeout),
            )

to

            response: Response = await run_sync(lambda: session.request(
                method,
                url,
                params=params,
                data=data,
                headers=headers,
                json=json,
                files=files,
                cookies=cookies,
                timeout=(connect_timeout, timeout),
            ))()

This all works fine except I've experienced a small but significant uptick in errors that I'm unable to reproduce locally. I can't tell right now if the errors come from some kind of timeout or some other edge case when working with requests + async + Quart's run_sync. If I use Quart, but just revert the session.request(...) to a synchronous call, the uptick in errors goes away.

I'm using gunicorn with UvicornWorker to run the app, using uvloop for the async framework.

It may line up with a CancelledError that I see in logs, but I'm not sure, and not sure why the session.request coroutine would be getting cancelled.

What might be causing this?


Solution

  • Aside from other smaller issues, I realized what I was missing is documented here. Quart throws a CancelledError whenever a client disconnects early. For my particular application this happens fairly regularly and I was misunderstanding how that works. An awaiting task far down in the stack will throw a CancelledError when its distant parent signals cancellation. So for example in a proxy application that waits for a call to a downstream, a CancelledError may often be raised when waiting for response headers from the downstream since that's where the bulk of the waiting time happens. This doesn't have anything to do with the downstream request, it's just a signal that the upstream client to the server is no longer connected.

    In my case I plan to handle these CancelledErrors at the top of the application and send back simple http 408 responses.

    This differs from Flask / other non-async frameworks.