Skip to content

Commit 19b3c5e

Browse files
Fix UnicodeDecodeError in aioredis integration (#3113)
Escape non-unicode bytes when decoding aioredis args. This prevents the `UnicodeDecodeError` while still offering some semblance of a human-readable string for tracing. fixes #3088 (cherry picked from commit 92390b2) Co-authored-by: Jameel Al-Aziz <[email protected]>
1 parent 083c598 commit 19b3c5e

8 files changed

+161
-7
lines changed

ddtrace/contrib/aioredis/patch.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ async def traced_execute_command(func, instance, args, kwargs):
6666
if not pin or not pin.enabled():
6767
return await func(*args, **kwargs)
6868

69-
decoded_args = [arg.decode() if isinstance(arg, bytes) else arg for arg in args]
70-
with _trace_redis_cmd(pin, config.aioredis, instance, decoded_args):
69+
with _trace_redis_cmd(pin, config.aioredis, instance, args):
7170
return await func(*args, **kwargs)
7271

7372

@@ -107,7 +106,6 @@ def traced_13_execute_command(func, instance, args, kwargs):
107106
if not pin or not pin.enabled():
108107
return func(*args, **kwargs)
109108

110-
decoded_args = [arg.decode() if isinstance(arg, bytes) else arg for arg in args]
111109
# Don't activate the span since this operation is performed as a future which concludes sometime later on in
112110
# execution so subsequent operations in the stack are not necessarily semantically related
113111
# (we don't want this span to be the parent of all other spans created before the future is resolved)
@@ -116,7 +114,7 @@ def traced_13_execute_command(func, instance, args, kwargs):
116114
)
117115

118116
span.set_tag(SPAN_MEASURED_KEY)
119-
query = format_command_args(decoded_args)
117+
query = format_command_args(args)
120118
span.resource = query
121119
span.set_tag(redisx.RAWCMD, query)
122120
if pin.tags:
@@ -129,7 +127,7 @@ def traced_13_execute_command(func, instance, args, kwargs):
129127
redisx.DB: instance.db or 0,
130128
}
131129
)
132-
span.set_metric(redisx.ARGS_LEN, len(decoded_args))
130+
span.set_metric(redisx.ARGS_LEN, len(args))
133131
# set analytics sample rate if enabled
134132
span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.aioredis.get_analytics_sample_rate())
135133

@@ -157,7 +155,7 @@ async def traced_13_execute_pipeline(func, instance, args, kwargs):
157155

158156
cmds = []
159157
for _, cmd, cmd_args, _ in instance._pipeline:
160-
parts = [cmd.decode() if isinstance(cmd, bytes) else cmd]
158+
parts = [cmd]
161159
parts.extend(cmd_args)
162160
cmds.append(format_command_args(parts))
163161
resource = "\n".join(cmds)

ddtrace/contrib/redis/util.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from ...ext import SpanTypes
1010
from ...ext import net
1111
from ...ext import redis as redisx
12+
from ...internal.compat import binary_type
13+
from ...internal.compat import ensure_text
1214
from ...internal.compat import stringify
15+
from ...internal.compat import text_type
1316

1417

1518
VALUE_PLACEHOLDER = "?"
@@ -41,7 +44,10 @@ def format_command_args(args):
4144
out = []
4245
for arg in args:
4346
try:
44-
cmd = stringify(arg)
47+
if isinstance(arg, (binary_type, text_type)):
48+
cmd = ensure_text(arg, errors="backslashreplace")
49+
else:
50+
cmd = stringify(arg)
4551

4652
if len(cmd) > VALUE_MAX_LEN:
4753
cmd = cmd[:VALUE_MAX_LEN] + VALUE_TOO_LONG_MARK

ddtrace/internal/compat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
ensure_str = six.ensure_str
5252
stringify = six.text_type
5353
string_type = six.string_types[0]
54+
text_type = six.text_type
5455
binary_type = six.binary_type
5556
msgpack_type = six.binary_type
5657
# DEV: `six` doesn't have `float` in `integer_types`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Escape non-Unicode bytes when decoding aioredis args. This fixes a
5+
``UnicodeDecodeError`` that can be thrown from the aioredis integration
6+
when interacting with binary-encoded data, as is done in channels-redis.

tests/contrib/aioredis/test_aioredis.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,37 @@ async def test_basic_request(redis_client):
6666
assert val is None
6767

6868

69+
@pytest.mark.asyncio
70+
@pytest.mark.snapshot
71+
async def test_decoding_non_utf8_args(redis_client):
72+
await redis_client.set(b"\x80foo", b"\x80abc")
73+
val = await redis_client.get(b"\x80foo")
74+
assert val == b"\x80abc"
75+
76+
77+
@pytest.mark.asyncio
78+
@pytest.mark.snapshot(variants={"": aioredis_version >= (2, 0), "13": aioredis_version < (2, 0)})
79+
async def test_decoding_non_utf8_pipeline_args(redis_client):
80+
if aioredis_version >= (2, 0):
81+
p = await redis_client.pipeline(transaction=False)
82+
await p.set(b"\x80blah", "boo")
83+
await p.set("foo", b"\x80abc")
84+
await p.get(b"\x80blah")
85+
await p.get("foo")
86+
else:
87+
p = redis_client.pipeline()
88+
p.set(b"\x80blah", "boo")
89+
p.set("foo", b"\x80abc")
90+
p.get(b"\x80blah")
91+
p.get("foo")
92+
93+
response_list = await p.execute()
94+
assert response_list[0] is True # response from redis.set is OK if successfully pushed
95+
assert response_list[1] is True
96+
assert response_list[2].decode() == "boo"
97+
assert response_list[3] == b"\x80abc"
98+
99+
69100
@pytest.mark.asyncio
70101
@pytest.mark.snapshot
71102
async def test_long_command(redis_client):
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[[
2+
{
3+
"name": "redis.command",
4+
"service": "redis",
5+
"resource": "SET \\x80foo \\x80abc",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "redis",
10+
"meta": {
11+
"out.host": "127.0.0.1",
12+
"redis.raw_command": "SET \\x80foo \\x80abc",
13+
"runtime-id": "af49d807abff41089eed43f05d099627"
14+
},
15+
"metrics": {
16+
"_dd.agent_psr": 1.0,
17+
"_dd.measured": 1,
18+
"_dd.top_level": 1,
19+
"_dd.tracer_kr": 1.0,
20+
"_sampling_priority_v1": 1,
21+
"out.port": 6379,
22+
"out.redis_db": 0,
23+
"redis.args_length": 3,
24+
"system.pid": 36293
25+
},
26+
"duration": 6351000,
27+
"start": 1640819680842990000
28+
}],
29+
[
30+
{
31+
"name": "redis.command",
32+
"service": "redis",
33+
"resource": "GET \\x80foo",
34+
"trace_id": 1,
35+
"span_id": 1,
36+
"parent_id": 0,
37+
"type": "redis",
38+
"meta": {
39+
"out.host": "127.0.0.1",
40+
"redis.raw_command": "GET \\x80foo",
41+
"runtime-id": "af49d807abff41089eed43f05d099627"
42+
},
43+
"metrics": {
44+
"_dd.agent_psr": 1.0,
45+
"_dd.measured": 1,
46+
"_dd.top_level": 1,
47+
"_dd.tracer_kr": 1.0,
48+
"_sampling_priority_v1": 1,
49+
"out.port": 6379,
50+
"out.redis_db": 0,
51+
"redis.args_length": 2,
52+
"system.pid": 36293
53+
},
54+
"duration": 1959000,
55+
"start": 1640819680849456000
56+
}]]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[[
2+
{
3+
"name": "redis.command",
4+
"service": "redis",
5+
"resource": "SET \\x80blah boo\nSET foo \\x80abc\nGET \\x80blah\nGET foo",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "redis",
10+
"meta": {
11+
"out.host": "127.0.0.1",
12+
"redis.raw_command": "SET \\x80blah boo\nSET foo \\x80abc\nGET \\x80blah\nGET foo",
13+
"runtime-id": "e6362b13a7394c2490fc39fa29c0e4af"
14+
},
15+
"metrics": {
16+
"_dd.agent_psr": 1.0,
17+
"_dd.measured": 1,
18+
"_dd.top_level": 1,
19+
"_dd.tracer_kr": 1.0,
20+
"_sampling_priority_v1": 1,
21+
"out.port": 6379,
22+
"out.redis_db": 0,
23+
"redis.pipeline_length": 4,
24+
"system.pid": 60727
25+
},
26+
"duration": 922000,
27+
"start": 1640911687480019000
28+
}]]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[[
2+
{
3+
"name": "redis.command",
4+
"service": "redis",
5+
"resource": "SET \\x80blah boo\nSET foo \\x80abc\nGET \\x80blah\nGET foo",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "redis",
10+
"meta": {
11+
"out.host": "127.0.0.1",
12+
"redis.raw_command": "SET \\x80blah boo\nSET foo \\x80abc\nGET \\x80blah\nGET foo",
13+
"runtime-id": "2c1fc87edd3b44ea8e46fe3854500d3c"
14+
},
15+
"metrics": {
16+
"_dd.agent_psr": 1.0,
17+
"_dd.measured": 1,
18+
"_dd.top_level": 1,
19+
"_dd.tracer_kr": 1.0,
20+
"_sampling_priority_v1": 1,
21+
"out.port": 6379,
22+
"out.redis_db": 0,
23+
"redis.pipeline_length": 4,
24+
"system.pid": 60927
25+
},
26+
"duration": 982000,
27+
"start": 1640911730323059000
28+
}]]

0 commit comments

Comments
 (0)