{"record":{"id":"8eec40b015078dca","repo":"aio-libs/aiohttp","slug":"connection-lost-8eec40","errorCode":null,"errorMessage":"Connection lost","messagePattern":"Connection lost","errorType":"exception","errorClass":"ConnectionResetError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_fileresponse.py","lineNumber":137,"sourceCode":"                break\n            chunk = await loop.run_in_executor(None, fobj.read, min(chunk_size, count))\n\n        await writer.drain()\n        return writer\n\n    async def _sendfile(\n        self, request: \"BaseRequest\", fobj: BinaryIO, offset: int, count: int\n    ) -> AbstractStreamWriter:\n        writer = await super().prepare(request)\n        assert writer is not None\n\n        if NOSENDFILE or self.compression:\n            return await self._sendfile_fallback(writer, fobj, offset, count)\n\n        loop = request._loop\n        transport = request.transport\n        if transport is None:\n            raise ConnectionResetError(\"Connection lost\")\n\n        try:\n            if aiofastnet is not None:\n                await aiofastnet.sendfile(loop, transport, fobj, offset, count)\n            else:\n                await loop.sendfile(transport, fobj, offset, count)  # type: ignore[unreachable]\n        except NotImplementedError:\n            return await self._sendfile_fallback(writer, fobj, offset, count)\n\n        await super().write_eof()\n        return writer\n\n    @staticmethod\n    def _etag_match(etag_value: str, etags: tuple[ETag, ...], *, weak: bool) -> bool:\n        if len(etags) == 1 and etags[0].value == ETAG_ANY:\n            return True\n        return any(\n            etag.value == etag_value for etag in etags if weak or not etag.is_weak","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/web_fileresponse.py#L119-L155","documentation":"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.","triggerScenarios":"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.","commonSituations":"Mobile clients switching networks mid-download; browsers aborting large files; reverse-proxy/proxy read timeouts severing the upstream connection.","solutions":["Catch ConnectionResetError (and asyncio.CancelledError) around the handler that serves FileResponse and log at debug level.","Treat it as expected for downloads — do not return a 500.","Reduce the abort window with smaller files, Range support, or chunked streaming."],"exampleFix":"# before\nreturn web.FileResponse(path)\n# after\ntry:\n    return web.FileResponse(path)\nexcept ConnectionResetError:\n    logger.debug('client disconnected during file send')\n    return web.Response(status=499)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    return web.FileResponse(path)\nexcept ConnectionResetError:\n    logger.debug('client aborted file download')\n    return web.Response(status=499)","preventionTips":["Treat ConnectionResetError as expected for file downloads; log at debug.","Do not return 500 for client-initiated aborts.","Expose Range support so clients can resume instead of restarting."],"tags":["network","client-disconnect","fileresponse","connection-reset"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}