Skip to content

Commit f7a6598

Browse files
gerzsevladvildanov
authored andcommitted
Format connection errors in the same way everywhere (#3305)
Connection errors are formatted in four places, sync and async, network socket and unix socket. Each place has some small differences compared to the others, while they could be, and should be, formatted in an uniform way. Factor out the logic in a helper method and call that method in all four places. Arguably we lose some specificity, e.g. the words "unix socket" won't be there anymore, but it is more valuable to not have code duplication.
1 parent 2c39081 commit f7a6598

File tree

5 files changed

+111
-81
lines changed

5 files changed

+111
-81
lines changed

redis/asyncio/connection.py

+3-37
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
)
2626
from urllib.parse import ParseResult, parse_qs, unquote, urlparse
2727

28+
from ..utils import format_error_message
29+
2830
# the functionality is available in 3.11.x but has a major issue before
2931
# 3.11.3. See https://github.com/redis/redis-py/issues/2633
3032
if sys.version_info >= (3, 11, 3):
@@ -344,9 +346,8 @@ async def _connect(self):
344346
def _host_error(self) -> str:
345347
pass
346348

347-
@abstractmethod
348349
def _error_message(self, exception: BaseException) -> str:
349-
pass
350+
return format_error_message(self._host_error(), exception)
350351

351352
async def on_connect(self) -> None:
352353
"""Initialize the connection, authenticate and select a database"""
@@ -798,27 +799,6 @@ async def _connect(self):
798799
def _host_error(self) -> str:
799800
return f"{self.host}:{self.port}"
800801

801-
def _error_message(self, exception: BaseException) -> str:
802-
# args for socket.error can either be (errno, "message")
803-
# or just "message"
804-
805-
host_error = self._host_error()
806-
807-
if not exception.args:
808-
# asyncio has a bug where on Connection reset by peer, the
809-
# exception is not instanciated, so args is empty. This is the
810-
# workaround.
811-
# See: https://github.com/redis/redis-py/issues/2237
812-
# See: https://github.com/python/cpython/issues/94061
813-
return f"Error connecting to {host_error}. Connection reset by peer"
814-
elif len(exception.args) == 1:
815-
return f"Error connecting to {host_error}. {exception.args[0]}."
816-
else:
817-
return (
818-
f"Error {exception.args[0]} connecting to {host_error}. "
819-
f"{exception}."
820-
)
821-
822802

823803
class SSLConnection(Connection):
824804
"""Manages SSL connections to and from the Redis server(s).
@@ -970,20 +950,6 @@ async def _connect(self):
970950
def _host_error(self) -> str:
971951
return self.path
972952

973-
def _error_message(self, exception: BaseException) -> str:
974-
# args for socket.error can either be (errno, "message")
975-
# or just "message"
976-
host_error = self._host_error()
977-
if len(exception.args) == 1:
978-
return (
979-
f"Error connecting to unix socket: {host_error}. {exception.args[0]}."
980-
)
981-
else:
982-
return (
983-
f"Error {exception.args[0]} connecting to unix socket: "
984-
f"{host_error}. {exception.args[1]}."
985-
)
986-
987953

988954
FALSE_STRINGS = ("0", "F", "FALSE", "N", "NO")
989955

redis/connection.py

+2-37
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
HIREDIS_AVAILABLE,
4040
HIREDIS_PACK_AVAILABLE,
4141
SSL_AVAILABLE,
42+
format_error_message,
4243
get_lib_version,
4344
str_if_bytes,
4445
)
@@ -338,9 +339,8 @@ def _connect(self):
338339
def _host_error(self):
339340
pass
340341

341-
@abstractmethod
342342
def _error_message(self, exception):
343-
pass
343+
return format_error_message(self._host_error(), exception)
344344

345345
def on_connect(self):
346346
"Initialize the connection, authenticate and select a database"
@@ -733,27 +733,6 @@ def _connect(self):
733733
def _host_error(self):
734734
return f"{self.host}:{self.port}"
735735

736-
def _error_message(self, exception):
737-
# args for socket.error can either be (errno, "message")
738-
# or just "message"
739-
740-
host_error = self._host_error()
741-
742-
if len(exception.args) == 1:
743-
try:
744-
return f"Error connecting to {host_error}. \
745-
{exception.args[0]}."
746-
except AttributeError:
747-
return f"Connection Error: {exception.args[0]}"
748-
else:
749-
try:
750-
return (
751-
f"Error {exception.args[0]} connecting to "
752-
f"{host_error}. {exception.args[1]}."
753-
)
754-
except AttributeError:
755-
return f"Connection Error: {exception.args[0]}"
756-
757736

758737
class SSLConnection(Connection):
759738
"""Manages SSL connections to and from the Redis server(s).
@@ -930,20 +909,6 @@ def _connect(self):
930909
def _host_error(self):
931910
return self.path
932911

933-
def _error_message(self, exception):
934-
# args for socket.error can either be (errno, "message")
935-
# or just "message"
936-
host_error = self._host_error()
937-
if len(exception.args) == 1:
938-
return (
939-
f"Error connecting to unix socket: {host_error}. {exception.args[0]}."
940-
)
941-
else:
942-
return (
943-
f"Error {exception.args[0]} connecting to unix socket: "
944-
f"{host_error}. {exception.args[1]}."
945-
)
946-
947912

948913
FALSE_STRINGS = ("0", "F", "FALSE", "N", "NO")
949914

redis/utils.py

+12
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,15 @@ def get_lib_version():
141141
except metadata.PackageNotFoundError:
142142
libver = "99.99.99"
143143
return libver
144+
145+
146+
def format_error_message(host_error: str, exception: BaseException) -> str:
147+
if not exception.args:
148+
return f"Error connecting to {host_error}."
149+
elif len(exception.args) == 1:
150+
return f"Error {exception.args[0]} connecting to {host_error}."
151+
else:
152+
return (
153+
f"Error {exception.args[0]} connecting to {host_error}. "
154+
f"{exception.args[1]}."
155+
)

tests/test_asyncio/test_connection.py

+44-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
_AsyncRESPBase,
1313
)
1414
from redis.asyncio import ConnectionPool, Redis
15-
from redis.asyncio.connection import Connection, UnixDomainSocketConnection, parse_url
15+
from redis.asyncio.connection import (
16+
Connection,
17+
SSLConnection,
18+
UnixDomainSocketConnection,
19+
parse_url,
20+
)
1621
from redis.asyncio.retry import Retry
1722
from redis.backoff import NoBackoff
1823
from redis.exceptions import ConnectionError, InvalidResponse, TimeoutError
@@ -494,18 +499,50 @@ async def test_connection_garbage_collection(request):
494499

495500

496501
@pytest.mark.parametrize(
497-
"error, expected_message",
502+
"conn, error, expected_message",
498503
[
499-
(OSError(), "Error connecting to localhost:6379. Connection reset by peer"),
500-
(OSError(12), "Error connecting to localhost:6379. 12."),
504+
(SSLConnection(), OSError(), "Error connecting to localhost:6379."),
505+
(SSLConnection(), OSError(12), "Error 12 connecting to localhost:6379."),
501506
(
507+
SSLConnection(),
502508
OSError(12, "Some Error"),
503-
"Error 12 connecting to localhost:6379. [Errno 12] Some Error.",
509+
"Error 12 connecting to localhost:6379. Some Error.",
510+
),
511+
(
512+
UnixDomainSocketConnection(path="unix:///tmp/redis.sock"),
513+
OSError(),
514+
"Error connecting to unix:///tmp/redis.sock.",
515+
),
516+
(
517+
UnixDomainSocketConnection(path="unix:///tmp/redis.sock"),
518+
OSError(12),
519+
"Error 12 connecting to unix:///tmp/redis.sock.",
520+
),
521+
(
522+
UnixDomainSocketConnection(path="unix:///tmp/redis.sock"),
523+
OSError(12, "Some Error"),
524+
"Error 12 connecting to unix:///tmp/redis.sock. Some Error.",
504525
),
505526
],
506527
)
507-
async def test_connect_error_message(error, expected_message):
528+
async def test_format_error_message(conn, error, expected_message):
508529
"""Test that the _error_message function formats errors correctly"""
509-
conn = Connection()
510530
error_message = conn._error_message(error)
511531
assert error_message == expected_message
532+
533+
534+
async def test_network_connection_failure():
535+
with pytest.raises(ConnectionError) as e:
536+
redis = Redis(host="127.0.0.1", port=9999)
537+
await redis.set("a", "b")
538+
assert str(e.value).startswith("Error 111 connecting to 127.0.0.1:9999. Connect")
539+
540+
541+
async def test_unix_socket_connection_failure():
542+
with pytest.raises(ConnectionError) as e:
543+
redis = Redis(unix_socket_path="unix:///tmp/a.sock")
544+
await redis.set("a", "b")
545+
assert (
546+
str(e.value)
547+
== "Error 2 connecting to unix:///tmp/a.sock. No such file or directory."
548+
)

tests/test_connection.py

+50
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,53 @@ def mock_disconnect(_):
296296

297297
assert called == 1
298298
pool.disconnect()
299+
300+
301+
@pytest.mark.parametrize(
302+
"conn, error, expected_message",
303+
[
304+
(SSLConnection(), OSError(), "Error connecting to localhost:6379."),
305+
(SSLConnection(), OSError(12), "Error 12 connecting to localhost:6379."),
306+
(
307+
SSLConnection(),
308+
OSError(12, "Some Error"),
309+
"Error 12 connecting to localhost:6379. Some Error.",
310+
),
311+
(
312+
UnixDomainSocketConnection(path="unix:///tmp/redis.sock"),
313+
OSError(),
314+
"Error connecting to unix:///tmp/redis.sock.",
315+
),
316+
(
317+
UnixDomainSocketConnection(path="unix:///tmp/redis.sock"),
318+
OSError(12),
319+
"Error 12 connecting to unix:///tmp/redis.sock.",
320+
),
321+
(
322+
UnixDomainSocketConnection(path="unix:///tmp/redis.sock"),
323+
OSError(12, "Some Error"),
324+
"Error 12 connecting to unix:///tmp/redis.sock. Some Error.",
325+
),
326+
],
327+
)
328+
def test_format_error_message(conn, error, expected_message):
329+
"""Test that the _error_message function formats errors correctly"""
330+
error_message = conn._error_message(error)
331+
assert error_message == expected_message
332+
333+
334+
def test_network_connection_failure():
335+
with pytest.raises(ConnectionError) as e:
336+
redis = Redis(port=9999)
337+
redis.set("a", "b")
338+
assert str(e.value) == "Error 111 connecting to localhost:9999. Connection refused."
339+
340+
341+
def test_unix_socket_connection_failure():
342+
with pytest.raises(ConnectionError) as e:
343+
redis = Redis(unix_socket_path="unix:///tmp/a.sock")
344+
redis.set("a", "b")
345+
assert (
346+
str(e.value)
347+
== "Error 2 connecting to unix:///tmp/a.sock. No such file or directory."
348+
)

0 commit comments

Comments
 (0)