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.
Projectredis/redis-py
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
| 62 | 62 | ||
| 63 | 63 | def read_from_socket(self, timeout=SENTINEL, raise_on_timeout=True): | |
| 64 | 64 | 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) | |
| 65 | 72 | custom_timeout = timeout is not SENTINEL | |
| 66 | 73 | try: | |
| 67 | 74 | if custom_timeout: | |
| 68 | 75 | sock.settimeout(timeout) | |
| 69 | - | bufflen = self._sock.recv_into(self._buffer) | |
| 76 | + | bufflen = sock.recv_into(self._buffer) | |
| 70 | 77 | if bufflen == 0: | |
| 71 | 78 | raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) | |
| 72 | - | self._reader.feed(self._buffer, 0, bufflen) | |
| 79 | + | reader.feed(self._buffer, 0, bufflen) | |
| 73 | 80 | # data was read from the socket and added to the buffer. | |
| 74 | 81 | # return True to indicate that data was read. | |
| 75 | 82 | return True | |
| ⋯ | |||
| 99 | 106 | push_request=False, | |
| 100 | 107 | timeout: Union[float, object] = SENTINEL, | |
| 101 | 108 | ): | |
| 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: | |
| 103 | 114 | raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) | |
| 104 | 115 | ||
| 105 | 116 | if disable_decoding: | |
| 106 | - | response = self._reader.gets(False) | |
| 117 | + | response = reader.gets(False) | |
| 107 | 118 | else: | |
| 108 | - | response = self._reader.gets() | |
| 119 | + | response = reader.gets() | |
| 109 | 120 | ||
| 110 | 121 | while response is NOT_ENOUGH_DATA: | |
| 111 | 122 | self.read_from_socket(timeout=timeout) | |
| 112 | 123 | if disable_decoding: | |
| 113 | - | response = self._reader.gets(False) | |
| 124 | + | response = reader.gets(False) | |
| 114 | 125 | else: | |
| 115 | - | response = self._reader.gets() | |
| 126 | + | response = reader.gets() | |
| 116 | 127 | # if the response is a ConnectionError or the response is a list and | |
| 117 | 128 | # the first item is a ConnectionError, raise it as something bad | |
| 118 | 129 | # happened | |
The check that tells the two apart
fail→pass·tests/test_connection.py::test_hiredis_read_from_socket_raises_connection_error_when_disconnected[_reader]
fail→pass·tests/test_connection.py::test_hiredis_read_from_socket_raises_connection_error_when_disconnected[_sock]
fail→pass·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.