aio-libs/aiohttp · warning · ConnectionResetError

Connection lost

Error message

Connection lost

What it means

Raised as ConnectionResetError('Connection lost') in FileResponse._sendfile (aiohttp/web_fileresponse.py:137) when request.transport is None at the moment aiohttp is about to call loop.sendfile / aiofastnet.sendfile. A None transport means the underlying socket has already been closed (client disconnected or the protocol's transport was replaced) and there is no longer a file descriptor to send to. This is raised before any bytes are sent via sendfile(2).

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 c0ef574e29)

Solutions

  1. Treat ConnectionResetError / ConnectionError from FileResponse as expected and log at debug, not error - the client is already gone.
  2. Wrap file-serving handlers in try/except ConnectionError and return early.
  3. Tune upstream proxy timeouts (client_body_timeout, send_timeout) if disconnects happen mid-transfer on healthy clients.
  4. Verify the file path is valid before constructing FileResponse so unrelated failures don't masquerade as transport loss.

Example fix

// before
async def serve(request):
    return web.FileResponse(path)

# after
async def serve(request):
    try:
        return web.FileResponse(path)
    except (ConnectionResetError, ConnectionError):
        logger.debug("client gone during FileResponse(%s)", path)
        raise
Defensive patterns

Strategy: try-catch

Validate before calling

if request.transport is None:
    raise web.HTTPUnavailable()  # client already gone

Type guard

def transport_alive(request) -> bool:
    return request.transport is not None

Try / catch

try:
    return web.FileResponse(path)
except (ConnectionResetError, ConnectionError):
    logger.debug("client gone during FileResponse")
    raise

Prevention

When it happens

Trigger: Serving a static file with web.FileResponse(path) after the client has closed the connection (e.g. user aborted the download, or a proxy timed out and tore down the TCP connection). The handler still ran to the point of preparing the response, but transport was reset to None by the time _sendfile looked at it.

Common situations: Mobile clients on flaky networks, browsers cancelling downloads, or downstream proxies (nginx, CloudFront) with aggressive client-header-buffer / read timeouts. Also reproducible in tests that close the test client before the handler reaches sendfile.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/8eec40b015078dca.json. Report an issue: GitHub.