aio-libs/aiohttp · error · ConnectionResetError

Connection lost

Error message

Connection lost

What it means

In FileResponse._sendfile, aiohttp fetches request.transport and raises ConnectionResetError('Connection lost') if it is already None. This surfaces a client disconnect that happened before sendfile started, rather than passing None into loop.sendfile. It is a normal consequence of an aborted download, not a server bug.

Solutions

  1. Catch ConnectionResetError (and asyncio.CancelledError) around the handler that serves FileResponse and log at debug level.
  2. Treat it as expected for downloads — do not return a 500.
  3. Reduce the abort window with smaller files, Range support, or chunked streaming.

Example fix

# before
return web.FileResponse(path)
# after
try:
    return web.FileResponse(path)
except ConnectionResetError:
    logger.debug('client disconnected during file send')
    return web.Response(status=499)
Defensive patterns

Strategy: try-catch

Try / catch

try:
    return web.FileResponse(path)
except ConnectionResetError:
    logger.debug('client aborted file download')
    return web.Response(status=499)

Prevention

When it happens

Trigger: The client closes the socket (abort, network drop, navigation away) while FileResponse is still in prepare(), so by the time _sendfile runs request.transport is None.

Common situations: Mobile clients switching networks mid-download; browsers aborting large files; reverse-proxy/proxy read timeouts severing the upstream connection.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/8eec40b015078dca. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/web_fileresponse.py:137

                break
            chunk = await loop.run_in_executor(None, fobj.read, min(chunk_size, count))

        await writer.drain()
        return writer

    async def _sendfile(
        self, request: "BaseRequest", fobj: BinaryIO, offset: int, count: int
    ) -> AbstractStreamWriter:
        writer = await super().prepare(request)
        assert writer is not None

        if NOSENDFILE or self.compression:
            return await self._sendfile_fallback(writer, fobj, offset, count)

        loop = request._loop
        transport = request.transport
        if transport is None:
            raise ConnectionResetError("Connection lost")

        try:
            if aiofastnet is not None:
                await aiofastnet.sendfile(loop, transport, fobj, offset, count)
            else:
                await loop.sendfile(transport, fobj, offset, count)  # type: ignore[unreachable]
        except NotImplementedError:
            return await self._sendfile_fallback(writer, fobj, offset, count)

        await super().write_eof()
        return writer

    @staticmethod
    def _etag_match(etag_value: str, etags: tuple[ETag, ...], *, weak: bool) -> bool:
        if len(etags) == 1 and etags[0].value == ETAG_ANY:
            return True
        return any(
            etag.value == etag_value for etag in etags if weak or not etag.is_weak

View on GitHub (pinned to d041d4d0fd)