aiohttp_retry icon indicating copy to clipboard operation
aiohttp_retry copied to clipboard

Add retry support for reading response body when chunked transfer encoding is enabled

Open alexdrydew opened this issue 3 years ago • 0 comments

Hello, here is an example I have been struggling with recently. When chunked transfer encoding is enabled on the server RetryClient isn't able to retry request in case of responses headers being correct but a problem occurred during body transmission (i.e. server lagged).

Here is a minimal example of the problem:

Server sleeps for 10 seconds during response if not retried enough:

class RetryCountingHandler:
    def __init__(self, retries_before_response):
        self.retries_left = retries_before_response

    async def get_response(self, request):
        response = web.StreamResponse()
        response.enable_chunked_encoding()
        await response.prepare(request)
        self.retries_left -= 1
        if self.retries_left >= 0:
            await asyncio.sleep(10)
        await response.write('Hello world'.encode('utf-8'))
        return response

    async def get_retries_left(self, request):
        return web.Response(text=str(self.retries_left))

    async def ping(self, request):
        return web.Response(text='OK')


def run_server(url, port):
    app = web.Application()
    handler = RetryCountingHandler(retries_before_response=10)
    app.add_routes([
        web.get('/response', handler.get_response),
        web.get('/retries_left', handler.get_retries_left),
        web.get('/ping', handler.ping)
    ])
    web.run_app(app, host=url, port=port)

Client side is a simple RetryClient:

async def get_text():
    async with RetryClient(retry_options=aiohttp_retry.ListRetry([0.1] * 10, exceptions={Exception})) as session:
        response = await session.get(f'{url}/response', timeout=0.1)
        try:
            print(await response.text())
        except:
            response = await session.get(f'{url}/retries_left')
            print(await response.text())
            raise

asyncio.run(get_text())

In this case a timeout exception will be raised during response.text() but actual attempts count is 1.

Currently using urllib3 retries in requests library leads to the same behaviour but I believe that this functionality is especially important for retries in context of asynchronous library and this feature will be a strong reason to finally completely move from urllib3 to aiohttp.

The full minimal example is available in gist: https://gist.github.com/alexdrydew/c68a1b999ec57503e28f851cedf8e0eb

alexdrydew avatar Sep 21 '21 08:09 alexdrydew