Skip to content

Commit a084c2c

Browse files
rocky-kenemdnetoxrmx
authored
Add fallback decoding for asgi headers (#2837)
* Add latin-1 fallback decoding for asgi headers * Add comment for ASGI encoding spec and change to unicode_escape * add unit test for non-utf8 header decoding * add changelog * revert lint * code review changes * Fix changelog * Add ASGIGetter test --------- Co-authored-by: Emídio Neto <[email protected]> Co-authored-by: Riccardo Magliocchetti <[email protected]>
1 parent 3deb6b9 commit a084c2c

File tree

4 files changed

+50
-6
lines changed

4 files changed

+50
-6
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
([#2537](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2537))
2424
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi` Add ability to disable internal HTTP send and receive spans
2525
([#2802](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2802))
26+
- `opentelemetry-instrumentation-asgi` Add fallback decoding for ASGI headers
27+
([#2837](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2837))
2628

2729
### Breaking changes
2830

instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -284,17 +284,17 @@ def get(
284284
# ASGI header keys are in lower case
285285
key = key.lower()
286286
decoded = [
287-
_value.decode("utf8")
287+
_decode_header_item(_value)
288288
for (_key, _value) in headers
289-
if _key.decode("utf8").lower() == key
289+
if _decode_header_item(_key).lower() == key
290290
]
291291
if not decoded:
292292
return None
293293
return decoded
294294

295295
def keys(self, carrier: dict) -> typing.List[str]:
296296
headers = carrier.get("headers") or []
297-
return [_key.decode("utf8") for (_key, _value) in headers]
297+
return [_decode_header_item(_key) for (_key, _value) in headers]
298298

299299

300300
asgi_getter = ASGIGetter()
@@ -410,7 +410,9 @@ def collect_custom_headers_attributes(
410410
if raw_headers:
411411
for key, value in raw_headers:
412412
# Decode headers before processing.
413-
headers[key.decode()].append(value.decode())
413+
headers[_decode_header_item(key)].append(
414+
_decode_header_item(value)
415+
)
414416

415417
return sanitize.sanitize_header_values(
416418
headers,
@@ -979,3 +981,13 @@ def _parse_active_request_count_attrs(
979981
_server_active_requests_count_attrs_new,
980982
sem_conv_opt_in_mode,
981983
)
984+
985+
986+
def _decode_header_item(value):
987+
try:
988+
return value.decode("utf-8")
989+
except ValueError:
990+
# ASGI header encoding specs, see:
991+
# - https://asgi.readthedocs.io/en/latest/specs/www.html#wsgi-encoding-differences (see: WSGI encoding differences)
992+
# - https://docs.python.org/3/library/codecs.html#text-encodings (see: Text Encodings)
993+
return value.decode("unicode_escape")

instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ async def http_app_with_custom_headers(scope, receive, send):
4848
b"my-custom-regex-value-3,my-custom-regex-value-4",
4949
),
5050
(b"my-secret-header", b"my-secret-value"),
51+
(
52+
b"non-utf8-header",
53+
b"Moto Z\xb2",
54+
),
55+
(
56+
b"Moto-Z\xb2-non-utf8-header-key",
57+
b"Moto Z\xb2",
58+
),
5159
],
5260
}
5361
)
@@ -130,6 +138,14 @@ async def test_http_custom_request_headers_in_span_attributes(self):
130138
(b"Regex-Test-Header-1", b"Regex Test Value 1"),
131139
(b"regex-test-header-2", b"RegexTestValue2,RegexTestValue3"),
132140
(b"My-Secret-Header", b"My Secret Value"),
141+
(
142+
b"non-utf8-header",
143+
b"Moto Z\xb2",
144+
),
145+
(
146+
b"Moto-Z\xb2-non-utf8-header-key",
147+
b"Moto Z\xb2",
148+
),
133149
]
134150
)
135151
self.seed_app(self.app)
@@ -147,6 +163,8 @@ async def test_http_custom_request_headers_in_span_attributes(self):
147163
"http.request.header.regex_test_header_2": (
148164
"RegexTestValue2,RegexTestValue3",
149165
),
166+
"http.request.header.non_utf8_header": ("Moto Z²",),
167+
"http.request.header.moto_z²_non_utf8_header_key": ("Moto Z²",),
150168
"http.request.header.my_secret_header": ("[REDACTED]",),
151169
}
152170
for span in span_list:
@@ -223,6 +241,8 @@ async def test_http_custom_response_headers_in_span_attributes(self):
223241
"my-custom-regex-value-3,my-custom-regex-value-4",
224242
),
225243
"http.response.header.my_secret_header": ("[REDACTED]",),
244+
"http.response.header.non_utf8_header": ("Moto Z²",),
245+
"http.response.header.moto_z²_non_utf8_header_key": ("Moto Z²",),
226246
}
227247
for span in span_list:
228248
if span.kind == SpanKind.SERVER:
@@ -418,8 +438,8 @@ async def test_websocket_custom_response_headers_not_in_span_attributes(
418438

419439

420440
SANITIZE_FIELDS_TEST_VALUE = ".*my-secret.*"
421-
SERVER_REQUEST_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*"
422-
SERVER_RESPONSE_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*"
441+
SERVER_REQUEST_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*,non-utf8-header,Moto-Z²-non-utf8-header-key"
442+
SERVER_RESPONSE_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*,non-utf8-header,Moto-Z²-non-utf8-header-key"
423443

424444

425445
class TestCustomHeadersEnv(TestCustomHeaders):

instrumentation/opentelemetry-instrumentation-asgi/tests/test_getter.py

+10
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,13 @@ def test_keys(self):
6969
expected_val,
7070
"Should be equal",
7171
)
72+
73+
def test_non_utf8_headers(self):
74+
getter = ASGIGetter()
75+
carrier = {"headers": [(b"test-key", b"Moto Z\xb2")]}
76+
expected_val = ["Moto Z²"]
77+
self.assertEqual(
78+
getter.get(carrier, "test-key"),
79+
expected_val,
80+
"Should be equal",
81+
)

0 commit comments

Comments
 (0)