Skip to content

Commit c1a3202

Browse files
committed
Truncate pipeline exception message to a sane size
Fixes #20234.
1 parent 09b1376 commit c1a3202

File tree

8 files changed

+99
-5
lines changed

8 files changed

+99
-5
lines changed

redis/asyncio/client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1508,9 +1508,17 @@ def annotate_exception(
15081508
self, exception: Exception, number: int, command: Iterable[object]
15091509
) -> None:
15101510
cmd = " ".join(map(safe_str, command))
1511-
msg = f"Command # {number} ({cmd}) of pipeline caused error: {exception.args}"
1511+
msg = (
1512+
f"Command # {number} ({self._truncate_command(cmd)}) "
1513+
"of pipeline caused error: {exception.args}"
1514+
)
15121515
exception.args = (msg,) + exception.args[1:]
15131516

1517+
def _truncate_command(self, command, max_length=100):
1518+
if len(command) > max_length:
1519+
return command[: max_length - 3] + "..."
1520+
return command
1521+
15141522
async def parse_response(
15151523
self, connection: Connection, command_name: Union[str, bytes], **options
15161524
):

redis/asyncio/cluster.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,8 +1598,9 @@ async def _execute(
15981598
if isinstance(result, Exception):
15991599
command = " ".join(map(safe_str, cmd.args))
16001600
msg = (
1601-
f"Command # {cmd.position + 1} ({command}) of pipeline "
1602-
f"caused error: {result.args}"
1601+
f"Command # {cmd.position + 1} "
1602+
f"({self._truncate_command(command)}) "
1603+
f"of pipeline caused error: {result.args}"
16031604
)
16041605
result.args = (msg,) + result.args[1:]
16051606
raise result
@@ -1648,6 +1649,11 @@ def mset_nonatomic(
16481649

16491650
return self
16501651

1652+
def _truncate_command(self, command, max_length=100):
1653+
if len(command) > max_length:
1654+
return command[: max_length - 3] + "..."
1655+
return command
1656+
16511657

16521658
for command in PIPELINE_BLOCKED_COMMANDS:
16531659
command = command.replace(" ", "_").lower()

redis/client.py

100755100644
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1521,10 +1521,16 @@ def raise_first_error(self, commands, response):
15211521
def annotate_exception(self, exception, number, command):
15221522
cmd = " ".join(map(safe_str, command))
15231523
msg = (
1524-
f"Command # {number} ({cmd}) of pipeline caused error: {exception.args[0]}"
1524+
f"Command # {number} ({self._truncate_command(cmd)}) of pipeline "
1525+
f"caused error: {exception.args[0]}"
15251526
)
15261527
exception.args = (msg,) + exception.args[1:]
15271528

1529+
def _truncate_command(self, command, max_length=100):
1530+
if len(command) > max_length:
1531+
return command[: max_length - 3] + "..."
1532+
return command
1533+
15281534
def parse_response(self, connection, command_name, **options):
15291535
result = Redis.parse_response(self, connection, command_name, **options)
15301536
if command_name in self.UNWATCH_COMMANDS:

redis/cluster.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2054,10 +2054,16 @@ def annotate_exception(self, exception, number, command):
20542054
"""
20552055
cmd = " ".join(map(safe_str, command))
20562056
msg = (
2057-
f"Command # {number} ({cmd}) of pipeline caused error: {exception.args[0]}"
2057+
f"Command # {number} ({self._truncate_command(cmd)}) of pipeline "
2058+
f"caused error: {exception.args[0]}"
20582059
)
20592060
exception.args = (msg,) + exception.args[1:]
20602061

2062+
def _truncate_command(self, command, max_length=100):
2063+
if len(command) > max_length:
2064+
return command[: max_length - 3] + "x.."
2065+
return command
2066+
20612067
def execute(self, raise_on_error: bool = True) -> List[Any]:
20622068
"""
20632069
Execute all the commands in the current pipeline

tests/test_asyncio/test_cluster.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2802,6 +2802,25 @@ async def test_asking_error(self, r: RedisCluster) -> None:
28022802
assert ask_node._free.pop().read_response.await_count
28032803
assert res == ["MOCK_OK"]
28042804

2805+
async def test_error_is_truncated(self, r) -> None:
2806+
"""
2807+
Test that an error from the pipeline is truncated correctly.
2808+
"""
2809+
key = "a" * 5000
2810+
2811+
async with r.pipeline() as pipe:
2812+
pipe.set(key, 1)
2813+
pipe.llen(key)
2814+
pipe.expire(key, 100)
2815+
2816+
with pytest.raises(Exception) as ex:
2817+
await pipe.execute()
2818+
2819+
expected = (
2820+
"Command # 2 (LLEN " + ("a" * 92) + "...) of pipeline caused error: "
2821+
)
2822+
assert str(ex.value).startswith(expected)
2823+
28052824
async def test_moved_redirection_on_slave_with_default(
28062825
self, r: RedisCluster
28072826
) -> None:

tests/test_asyncio/test_pipeline.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,21 @@ async def test_exec_error_in_no_transaction_pipeline_unicode_command(self, r):
368368

369369
assert await r.get(key) == b"1"
370370

371+
async def test_exec_error_in_pipeline_truncated(self, r):
372+
key = "a" * 5000
373+
await r.set(key, 1)
374+
async with r.pipeline(transaction=False) as pipe:
375+
pipe.llen(key)
376+
pipe.expire(key, 100)
377+
378+
with pytest.raises(redis.ResponseError) as ex:
379+
await pipe.execute()
380+
381+
expected = (
382+
"Command # 1 (LLEN " + ("a" * 92) + "...) of pipeline caused error: "
383+
)
384+
assert str(ex.value).startswith(expected)
385+
371386
async def test_pipeline_with_bitfield(self, r):
372387
async with r.pipeline() as pipe:
373388
pipe.set("a", "1")

tests/test_cluster.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3260,6 +3260,25 @@ def raise_ask_error():
32603260
assert ask_node.redis_connection.connection.read_response.called
32613261
assert res == ["MOCK_OK"]
32623262

3263+
def test_error_is_truncated(self, r):
3264+
"""
3265+
Test that an error from the pipeline is truncated correctly.
3266+
"""
3267+
key = "a" * 5000
3268+
3269+
with r.pipeline() as pipe:
3270+
pipe.set(key, 1)
3271+
pipe.llen(key)
3272+
pipe.expire(key, 100)
3273+
3274+
with pytest.raises(Exception) as ex:
3275+
pipe.execute()
3276+
3277+
expected = (
3278+
"Command # 2 (LLEN " + ("a" * 92) + "...) of pipeline caused error: "
3279+
)
3280+
assert str(ex.value).startswith(expected)
3281+
32633282
def test_return_previously_acquired_connections(self, r):
32643283
# in order to ensure that a pipeline will make use of connections
32653284
# from different nodes

tests/test_pipeline.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,21 @@ def test_exec_error_in_no_transaction_pipeline_unicode_command(self, r):
369369

370370
assert r[key] == b"1"
371371

372+
def test_exec_error_in_pipeline_truncated(self, r):
373+
key = "a" * 5000
374+
r[key] = 1
375+
with r.pipeline(transaction=False) as pipe:
376+
pipe.llen(key)
377+
pipe.expire(key, 100)
378+
379+
with pytest.raises(redis.ResponseError) as ex:
380+
pipe.execute()
381+
382+
expected = (
383+
"Command # 1 (LLEN " + ("a" * 92) + "...) of pipeline caused error: "
384+
)
385+
assert str(ex.value).startswith(expected)
386+
372387
def test_pipeline_with_bitfield(self, r):
373388
with r.pipeline() as pipe:
374389
pipe.set("a", "1")

0 commit comments

Comments
 (0)