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
- 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.
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
- 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.
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
- Cannot write to closing transport
- Connection lost
- Connection lost
- Cannot connect to unix socket
- Cannot write to closing transport
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_weakView on GitHub (pinned to d041d4d0fd)