{"id":"cd7147f89f16e38d","repo":"redis/redis-py","slug":"connection-has-data","errorCode":null,"errorMessage":"Connection has data","messagePattern":"Connection has data","errorType":"exception","errorClass":"ConnectionError","httpStatus":null,"severity":"warning","filePath":"redis/asyncio/connection.py","lineNumber":2873,"sourceCode":"            decode_responses=kwargs.get(\"decode_responses\", False),\n        )\n\n    def make_connection(self):\n        \"\"\"Create a new connection.  Can be overridden by child classes.\"\"\"\n        # Note: We don't record IDLE here because async uses a sync make_connection\n        # but async record_connection_count. The recording is handled in get_connection.\n        return self.connection_class(**self.connection_kwargs)\n\n    async def ensure_connection(self, connection: AbstractConnection):\n        \"\"\"Ensure that the connection object is connected and valid\"\"\"\n        await connection.connect()\n        # connections that the pool provides should be ready to send\n        # a command. if not, the connection was either returned to the\n        # pool before all data has been read or the socket has been\n        # closed. either way, reconnect and verify everything is good.\n        try:\n            if await connection.can_read() and not self.maint_notifications_enabled():\n                raise ConnectionError(\"Connection has data\") from None\n        except (ConnectionError, TimeoutError, OSError):\n            await connection.disconnect()\n            await connection.connect()\n            if await connection.can_read() and not self.maint_notifications_enabled():\n                raise ConnectionError(\"Connection not ready\") from None\n\n    async def release(self, connection: AbstractConnection):\n        \"\"\"Releases the connection back to the pool\"\"\"\n        # Connections should always be returned to the correct pool,\n        # not doing so is an error that will cause an exception here.\n        async with self._lock:\n            self._in_use_connections.remove(connection)\n\n            if connection.should_reconnect():\n                await connection.disconnect()\n\n            self._available_connections.append(connection)\n","sourceCodeStart":2855,"sourceCodeEnd":2891,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/connection.py#L2855-L2891","documentation":"Raised by ensure_connection() when a pooled connection returned to the caller still has unread data on the socket (can_read() is True) and maintenance notifications are not enabled. A healthy pooled connection should be drained; leftover bytes mean a previous response was not fully read (or the socket is in a bad state), so the pool treats it as corrupt and refuses to hand it out.","triggerScenarios":"A connection is returned to the pool before its previous response was fully consumed (e.g., an interrupted/partial read, a cancelled task mid-command), then retrieved again and checked in ensure_connection(). The first can_read() check at connection.py:2872 trips it.","commonSituations":"Task cancellation in the middle of reading a response; a custom command path that returns the connection early; protocol desync after a network blip; rarely, a parser bug.","solutions":["Always fully consume command responses before the connection is released back to the pool (let the client API manage release).","Guard against task cancellation around commands (use asyncio.shield or structured concurrency) so a response is not left half-read.","If it recurs, set a lower socket timeout or enable health-check reconnection; report a parser bug if it persists with stock usage."],"exampleFix":"// before\nconn = await pool.get_connection()\nawait conn.send_command('GET', 'k')\n# task cancelled before reading response -> conn returned with data\n// after\nconn = await pool.get_connection()\ntry:\n    await conn.send_command('GET', 'k')\n    resp = await conn.read_response()\nfinally:\n    await pool.release(conn)","handlingStrategy":"try-catch","validationCode":"# prefer the high-level API which manages release/drain for you\nasync with redis.client('GET', 'k') as r:\n    val = await r","typeGuard":"def connection_is_clean(conn) -> bool:\n    import asyncio\n    try:\n        return not conn.can_read() if hasattr(conn, 'can_read') else True\n    except Exception:\n        return False","tryCatchPattern":"from redis.exceptions import ConnectionError\ntry:\n    await pool.get_connection('GET')\nexcept ConnectionError as e:\n    if 'Connection has data' in str(e):\n        await pool.disconnect()\n        # retry; pool will build a fresh connection","preventionTips":["Let the client API fully read responses before release.","Avoid cancelling tasks mid-command; use asyncio.shield for critical reads.","Use async with redis: to guarantee release."],"tags":["pool","connection-health","protocol","asyncio"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}