Skip to content

Commit 70af55c

Browse files
committed
fix(aiohttp): Preserve SigV4-signed propagation headers
- collect headers that must not be modified in `headers_to_skip`. - preserve existing `sentry-trace` headers to avoid replacing the current trace context. - leave signed `sentry-trace` and `baggage` headers untouched; missing propagation headers are added. - tests added for signed, unsigned and presigned requests. Resolves #7426
1 parent 42aa532 commit 70af55c

2 files changed

Lines changed: 137 additions & 16 deletions

File tree

‎sentry_sdk/integrations/aiohttp.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
)
3030
from sentry_sdk.tracing import (
3131
BAGGAGE_HEADER_NAME,
32+
SENTRY_TRACE_HEADER_NAME,
3233
SOURCE_FOR_STYLE,
3334
TransactionSource,
3435
)
@@ -43,6 +44,8 @@
4344
HAS_REAL_CONTEXTVARS,
4445
SENSITIVE_DATA_SUBSTITUTE,
4546
AnnotatedValue,
47+
_get_aws_sigv4_signed_headers_from_authorization_header,
48+
_get_aws_sigv4_signed_headers_from_url_query_string,
4649
_register_control_flow_exception,
4750
capture_internal_exceptions,
4851
ensure_integration_enabled,
@@ -81,6 +84,7 @@
8184

8285

8386
TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
87+
_SENTRY_HEADER_NAMES = frozenset((BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME))
8488

8589

8690
class AioHttpIntegration(Integration):
@@ -464,12 +468,50 @@ async def on_request_start(
464468
span = legacy_span
465469

466470
if should_propagate_trace(client, str(params.url)):
471+
# existing `sentry-trace`: skip so it is not duplicated.
472+
headers_to_skip: "set[str]" = set()
473+
if SENTRY_TRACE_HEADER_NAME in params.headers:
474+
headers_to_skip.add(SENTRY_TRACE_HEADER_NAME)
475+
476+
with capture_internal_exceptions():
477+
authorization = params.headers.get("Authorization")
478+
if authorization:
479+
if isinstance(authorization, bytes):
480+
authorization = authorization.decode("latin-1")
481+
# `SignedHeaders` lists fields covered by SigV4.
482+
signed_headers = (
483+
_get_aws_sigv4_signed_headers_from_authorization_header(
484+
authorization
485+
)
486+
)
487+
# skip signed `sentry-trace` and `baggage`.
488+
headers_to_skip.update(
489+
_SENTRY_HEADER_NAMES.intersection(signed_headers)
490+
)
491+
492+
# presigned URLs list signed names in the query string.
493+
query_signed_headers = (
494+
_get_aws_sigv4_signed_headers_from_url_query_string(str(params.url))
495+
)
496+
headers_to_skip.update(
497+
_SENTRY_HEADER_NAMES.intersection(query_signed_headers)
498+
)
499+
467500
for (
468501
key,
469502
value,
470503
) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
471504
span=span
472505
):
506+
# skip signed headers and an existing `sentry-trace`.
507+
if key.lower() in headers_to_skip:
508+
logger.debug(
509+
"[Tracing] Not adding `{key}` header to outgoing request "
510+
"to {url}: it already exists or is covered by the AWS "
511+
"SigV4 signature.".format(key=key, url=params.url)
512+
)
513+
continue
514+
473515
logger.debug(
474516
"[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
475517
key=key, value=value, url=params.url

‎tests/integrations/aiohttp/test_aiohttp.py‎

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,9 @@ async def handler(request):
875875

876876

877877
@pytest.mark.asyncio
878-
async def test_outgoing_trace_headers(sentry_init, aiohttp_raw_server, aiohttp_client):
878+
async def test_outgoing_trace_headers_adds_missing_unsigned_propagation_headers(
879+
sentry_init, aiohttp_raw_server, aiohttp_client
880+
):
879881
sentry_init(
880882
integrations=[AioHttpIntegration()],
881883
traces_sample_rate=1.0,
@@ -903,36 +905,113 @@ async def handler(request):
903905
parent_span_id=request_span.span_id,
904906
sampled=1,
905907
)
908+
assert resp.request_info.headers["baggage"].count("sentry-trace_id=") == 1
906909

907910

908911
@pytest.mark.asyncio
909-
async def test_outgoing_trace_headers_append_to_baggage(
912+
async def test_outgoing_trace_headers_appends_baggage_but_preserves_sentry_trace(
910913
sentry_init, aiohttp_raw_server, aiohttp_client
911914
):
912915
sentry_init(
913916
integrations=[AioHttpIntegration()],
914917
traces_sample_rate=1.0,
915-
release="d08ebdb9309e1b004c6f52202de58a09c2268e42",
916918
)
917919

918920
async def handler(request):
919921
return web.Response(text="OK")
920922

921923
raw_server = await aiohttp_raw_server(handler)
922924

923-
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000):
924-
with start_transaction(
925-
name="/interactions/other-dogs/new-dog",
926-
op="greeting.sniff",
927-
trace_id="0123456789012345678901234567890",
928-
):
929-
client = await aiohttp_client(raw_server)
930-
resp = await client.get("/", headers={"bagGage": "custom=value"})
931-
932-
assert (
933-
resp.request_info.headers["baggage"]
934-
== "custom=value,sentry-trace_id=0123456789012345678901234567890,sentry-sample_rand=0.500000,sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true"
935-
)
925+
with start_transaction(name="test", sampled=True):
926+
client = await aiohttp_client(raw_server)
927+
resp = await client.get(
928+
"/",
929+
headers={
930+
"baggage": "vendor=value",
931+
"sentry-trace": "existing-trace",
932+
},
933+
)
934+
935+
headers = resp.request_info.headers
936+
# unsigned `baggage`: append to the existing value.
937+
assert headers["baggage"].startswith("vendor=value,")
938+
assert headers["baggage"].count("sentry-trace_id=") == 1
939+
# existing `sentry-trace`: leave as-is.
940+
assert headers["sentry-trace"] == "existing-trace"
941+
942+
943+
@pytest.mark.asyncio
944+
async def test_outgoing_trace_headers_preserves_signed_propagation_headers(
945+
sentry_init, aiohttp_raw_server, aiohttp_client
946+
):
947+
sentry_init(
948+
integrations=[AioHttpIntegration()],
949+
traces_sample_rate=1.0,
950+
)
951+
952+
async def handler(request):
953+
return web.Response(text="OK")
954+
955+
raw_server = await aiohttp_raw_server(handler)
956+
# both propagation headers are named in `SignedHeaders`.
957+
authorization = (
958+
"AWS4-HMAC-SHA256 "
959+
"Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, "
960+
"SignedHeaders=baggage;host;sentry-trace, "
961+
"Signature=sixtyseven"
962+
)
963+
964+
with start_transaction(name="test", sampled=True):
965+
client = await aiohttp_client(raw_server)
966+
resp = await client.get(
967+
"/",
968+
headers={
969+
"baggage": "vendor=value",
970+
"sentry-trace": "existing-trace",
971+
"Authorization": authorization,
972+
},
973+
)
974+
975+
headers = resp.request_info.headers
976+
# signed `baggage`: leave as-is.
977+
assert headers["baggage"] == "vendor=value"
978+
# signed `sentry-trace`: leave as-is.
979+
assert headers["sentry-trace"] == "existing-trace"
980+
981+
982+
@pytest.mark.asyncio
983+
async def test_outgoing_trace_headers_preserves_query_signed_baggage(
984+
sentry_init, aiohttp_raw_server, aiohttp_client
985+
):
986+
sentry_init(
987+
integrations=[AioHttpIntegration()],
988+
traces_sample_rate=1.0,
989+
)
990+
991+
async def handler(request):
992+
return web.Response(text="OK")
993+
994+
raw_server = await aiohttp_raw_server(handler)
995+
path = (
996+
"/"
997+
"?X-Amz-Algorithm=AWS4-HMAC-SHA256"
998+
"&X-Amz-Credential="
999+
"test%2F20260804%2Feu-west-1%2Fs3%2Faws4_request"
1000+
"&X-Amz-Date=20260804T120000Z"
1001+
"&X-Amz-Expires=60"
1002+
"&X-Amz-SignedHeaders=baggage%3Bhost"
1003+
"&X-Amz-Signature=sixtyseven"
1004+
)
1005+
1006+
with start_transaction(name="test", sampled=True):
1007+
client = await aiohttp_client(raw_server)
1008+
resp = await client.get(path, headers={"baggage": "vendor=value"})
1009+
1010+
headers = resp.request_info.headers
1011+
# query-signed `baggage`: leave as-is.
1012+
assert headers["baggage"] == "vendor=value"
1013+
# unsigned `sentry-trace`: add it.
1014+
assert "sentry-trace" in headers
9361015

9371016

9381017
@pytest.mark.asyncio

0 commit comments

Comments
 (0)