{"id":"4b1bb8b9a9c8ff7e","repo":"redis/redis-py","slug":"buffer-is-closed","errorCode":null,"errorMessage":"Buffer is closed.","messagePattern":"Buffer is closed\\.","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"redis/_parsers/base.py","lineNumber":549,"sourceCode":"        self._connected = True\n\n    def on_disconnect(self):\n        \"\"\"Called when the stream disconnects\"\"\"\n        self._connected = False\n\n    @deprecated_function(\n        version=\"8.0.0\",\n        reason=\"Use can_read() instead\",\n        name=\"can_read_destructive\",\n    )\n    async def can_read_destructive(self) -> bool:\n        return await self.can_read()\n\n    async def can_read(self) -> bool:\n        # TODO: Rename this API; it detects pending data or dirty/closed\n        # connection state, not only whether application data can be read.\n        if not self._connected:\n            raise OSError(\"Buffer is closed.\")\n        if self._buffer:\n            return True\n        # asyncio.StreamReader has no public non-destructive API for checking\n        # buffered bytes. Preserve dirty-connection detection for the Python\n        # parser and fail loudly if the private buffer API changes.\n        return bool(self._stream._buffer) or self._stream.at_eof()\n\n    async def _read(self, length: int) -> bytes:\n        \"\"\"\n        Read `length` bytes of data.  These are assumed to be followed\n        by a '\\r\\n' terminator which is subsequently discarded.\n        \"\"\"\n        want = length + 2\n        end = self._pos + want\n        if len(self._buffer) >= end:\n            result = self._buffer[self._pos : end - 2]\n        else:\n            tail = self._buffer[self._pos :]","sourceCodeStart":531,"sourceCodeEnd":567,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/_parsers/base.py#L531-L567","documentation":"Raised by _AsyncRESPBase.can_read() (the async Python parser) when self._connected is False. can_read() is called by PubSub/health checks to detect pending data on a connection; calling it after on_disconnect() set _connected=False is undefined, so the parser raises OSError('Buffer is closed.') Note this is an OSError, not a ConnectionError - the connection object is in a terminal state and should not be reused.","triggerScenarios":"Calling pubsub.get_message(), connection.can_read(), or a pipeline health check on a connection that was already disconnected (server-initiated close, client.disconnect(), pool eviction, or 'with client:' context exit). The reader/writer were torn down and _connected flipped to False before can_read ran.","commonSituations":"Holding a PubSub object across a reconnect and calling get_message() before re-subscribing; using 'async with redis.Redis()' then awaiting a method after the block exited; server-side idle timeout (tcp-keepalive/tcp-timeout) dropping the connection between polls.","solutions":["Check pubsub.connection or the client's connection state before polling, or catch OSError and resubscribe on a fresh connection.","Use the high-level pubsub context manager / reconnect helpers so a new connection is acquired automatically.","Raise server tcp-keepalive/timeout values or send periodic PING commands to keep the connection alive.","Do not reuse a PubSub or connection object after an explicit disconnect() or context-manager exit."],"exampleFix":"// before\nps = r.pubsub()\nawait ps.subscribe(\"ch\")\n# ... connection drops, then:\nmsg = await ps.get_message()  # OSError: Buffer is closed.\n\n// after\ntry:\n    msg = await ps.get_message(timeout=1)\nexcept OSError:\n    await ps.close()\n    ps = r.pubsub()\n    await ps.subscribe(\"ch\")\n    msg = await ps.get_message(timeout=1)","handlingStrategy":"try-catch","validationCode":"# Before polling pubsub, check connection liveness\nasync def pubsub_alive(ps):\n    conn = getattr(ps, \"connection\", None)\n    return conn is not None and getattr(conn, \"_reader\", None) is not None","typeGuard":null,"tryCatchPattern":"try:\n    msg = await ps.get_message(timeout=1)\nexcept OSError as e:\n    if \"Buffer is closed\" in str(e):\n        await ps.close()\n        ps = r.pubsub()\n        await ps.subscribe(*channels)","preventionTips":["Never call pubsub.get_message() after explicit disconnect() or context-manager exit.","Resubscribe on a fresh PubSub after any ConnectionError.","Keep the connection alive with periodic PINGs if idle periods are long.","Treat OSError('Buffer is closed.') as terminal - do not retry on the same object."],"tags":["async","pubsub","connection","lifecycle"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}