One function

_HiredisParser in redis/redis-py

The author described this change as fix(hiredis): raise ConnectionError instead of AttributeError on concurrent disconnect (#4136). It counts as a record because the checks below fail on the code as it stood at bc0c81baf and pass on ef83cd8aa, with nothing else changed between the two runs.

Fix saved2026-07-03
Sharing licenceMIT · LICENSE
Change size+18 7

What the code was meant to do, written into the code itself as a docstring

Parser class for connections using Hiredis

The change

6262
6363 def read_from_socket(self, timeout=SENTINEL, raise_on_timeout=True):
6464 sock = self._sock
65+ reader = self._reader
66+ # Another thread may disconnect this connection while we are here (e.g.
67+ # a shared client closed via `with redis:`); on_disconnect() sets both
68+ # _sock and _reader to None. Bind them locally and fail with a
69+ # descriptive, retryable ConnectionError instead of an AttributeError.
70+ if sock is None or reader is None:
71+ raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
6572 custom_timeout = timeout is not SENTINEL
6673 try:
6774 if custom_timeout:
6875 sock.settimeout(timeout)
69- bufflen = self._sock.recv_into(self._buffer)
76+ bufflen = sock.recv_into(self._buffer)
7077 if bufflen == 0:
7178 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
72- self._reader.feed(self._buffer, 0, bufflen)
79+ reader.feed(self._buffer, 0, bufflen)
7380 # data was read from the socket and added to the buffer.
7481 # return True to indicate that data was read.
7582 return True
99106 push_request=False,
100107 timeout: Union[float, object] = SENTINEL,
101108 ):
102- if not self._reader:
109+ # Bind the reader locally so a concurrent disconnect that clears
110+ # self._reader can't turn a later .gets() into an AttributeError;
111+ # re-checking the attribute each time would still race.
112+ reader = self._reader
113+ if reader is None:
103114 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
104115
105116 if disable_decoding:
106- response = self._reader.gets(False)
117+ response = reader.gets(False)
107118 else:
108- response = self._reader.gets()
119+ response = reader.gets()
109120
110121 while response is NOT_ENOUGH_DATA:
111122 self.read_from_socket(timeout=timeout)
112123 if disable_decoding:
113- response = self._reader.gets(False)
124+ response = reader.gets(False)
114125 else:
115- response = self._reader.gets()
126+ response = reader.gets()
116127 # if the response is a ConnectionError or the response is a list and
117128 # the first item is a ConnectionError, raise it as something bad
118129 # happened

The check that tells the two apart

failpass·tests/test_connection.py::test_hiredis_read_from_socket_raises_connection_error_when_disconnected[_reader]
failpass·tests/test_connection.py::test_hiredis_read_from_socket_raises_connection_error_when_disconnected[_sock]
failpass·tests/test_connection.py::test_hiredis_read_response_uses_local_reader_if_disconnected_mid_read

Check file tests/test_connection.py, taken without changes from the fix and copied onto the older code, so the exact same check runs against both versions.

Origin and history

The code before itbc0c81baf44c9d58b25031d47c2e6156be88e087
Broken version dated2026-07-02
Moduleredis._parsers.hiredis
Units changed_HiredisParser
Fingerprintbbd9e70d0330b778
Checked2026-08-18 by goldset/0.1

Every field above is generated by our program. None of it is written by hand.

Other bugs found in redis/redis-py