Skip to content

Commit f4d4af7

Browse files
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 Co-authored-by: Pablo Deputter <pablo.deputter@sentry.io>
1 parent 5e2fcf1 commit f4d4af7

4 files changed

Lines changed: 134 additions & 9 deletions

File tree

‎sentry_sdk/consts.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
DEFAULT_MAX_STACK_FRAMES = 100
88
DEFAULT_ADD_FULL_STACK = False
99

10+
_SENTRY_HEADER_NAMES = frozenset(("baggage", "sentry-trace"))
11+
1012

1113
# Also needs to be at the top to prevent circular import
1214
class EndpointType(Enum):

‎sentry_sdk/integrations/aiohttp.py‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import sentry_sdk
66
from sentry_sdk.api import continue_trace
7-
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
7+
from sentry_sdk.consts import _SENTRY_HEADER_NAMES, OP, SPANDATA, SPANSTATUS
88
from sentry_sdk.data_collection import (
99
_apply_data_collection_filtering_to_query_string,
1010
)
@@ -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,
@@ -464,12 +467,50 @@ async def on_request_start(
464467
span = legacy_span
465468

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

‎sentry_sdk/integrations/stdlib.py‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
from typing import TYPE_CHECKING
77

88
import sentry_sdk
9-
from sentry_sdk.consts import OP, SPANDATA
9+
from sentry_sdk.consts import _SENTRY_HEADER_NAMES, OP, SPANDATA
1010
from sentry_sdk.integrations import Integration
1111
from sentry_sdk.scope import add_global_event_processor
1212
from sentry_sdk.traces import StreamedSpan
13-
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, Span
13+
from sentry_sdk.tracing import SENTRY_TRACE_HEADER_NAME, Span
1414
from sentry_sdk.tracing_utils import (
1515
EnvironHeaders,
1616
add_http_breadcrumb,
@@ -42,9 +42,6 @@
4242
"version": "%s.%s.%s" % (sys.version_info[:3]),
4343
"build": sys.version,
4444
}
45-
46-
_SENTRY_HEADER_NAMES = frozenset((BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME))
47-
4845
try:
4946
from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection
5047
except ImportError:

‎tests/integrations/aiohttp/test_aiohttp.py‎

Lines changed: 88 additions & 3 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,10 +905,11 @@ 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(
@@ -927,12 +930,94 @@ async def handler(request):
927930
trace_id="0123456789012345678901234567890",
928931
):
929932
client = await aiohttp_client(raw_server)
930-
resp = await client.get("/", headers={"bagGage": "custom=value"})
933+
resp = await client.get(
934+
"/",
935+
headers={
936+
"bagGage": "custom=value",
937+
"Sentry-Trace": "existing-trace",
938+
},
939+
)
931940

932941
assert (
933942
resp.request_info.headers["baggage"]
934943
== "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"
935944
)
945+
# existing `sentry-trace`: leave as-is.
946+
assert resp.request_info.headers["sentry-trace"] == "existing-trace"
947+
948+
949+
@pytest.mark.asyncio
950+
async def test_outgoing_trace_headers_preserves_signed_propagation_headers(
951+
sentry_init, aiohttp_raw_server, aiohttp_client
952+
):
953+
sentry_init(
954+
integrations=[AioHttpIntegration()],
955+
traces_sample_rate=1.0,
956+
)
957+
958+
async def handler(request):
959+
return web.Response(text="OK")
960+
961+
raw_server = await aiohttp_raw_server(handler)
962+
# both propagation headers are named in `SignedHeaders`.
963+
authorization = (
964+
"AWS4-HMAC-SHA256 "
965+
"Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, "
966+
"SignedHeaders=baggage;host;sentry-trace, "
967+
"Signature=sixtyseven"
968+
)
969+
970+
with start_transaction(name="test", sampled=True):
971+
client = await aiohttp_client(raw_server)
972+
resp = await client.get(
973+
"/",
974+
headers={
975+
"baggage": "vendor=value",
976+
"sentry-trace": "existing-trace",
977+
"Authorization": authorization,
978+
},
979+
)
980+
981+
headers = resp.request_info.headers
982+
# signed `baggage`: leave as-is.
983+
assert headers["baggage"] == "vendor=value"
984+
# signed `sentry-trace`: leave as-is.
985+
assert headers["sentry-trace"] == "existing-trace"
986+
987+
988+
@pytest.mark.asyncio
989+
async def test_outgoing_trace_headers_preserves_query_signed_baggage(
990+
sentry_init, aiohttp_raw_server, aiohttp_client
991+
):
992+
sentry_init(
993+
integrations=[AioHttpIntegration()],
994+
traces_sample_rate=1.0,
995+
)
996+
997+
async def handler(request):
998+
return web.Response(text="OK")
999+
1000+
raw_server = await aiohttp_raw_server(handler)
1001+
path = (
1002+
"/"
1003+
"?X-Amz-Algorithm=AWS4-HMAC-SHA256"
1004+
"&X-Amz-Credential="
1005+
"test%2F20260804%2Feu-west-1%2Fs3%2Faws4_request"
1006+
"&X-Amz-Date=20260804T120000Z"
1007+
"&X-Amz-Expires=60"
1008+
"&X-Amz-SignedHeaders=baggage%3Bhost"
1009+
"&X-Amz-Signature=sixtyseven"
1010+
)
1011+
1012+
with start_transaction(name="test", sampled=True):
1013+
client = await aiohttp_client(raw_server)
1014+
resp = await client.get(path, headers={"baggage": "vendor=value"})
1015+
1016+
headers = resp.request_info.headers
1017+
# query-signed `baggage`: leave as-is.
1018+
assert headers["baggage"] == "vendor=value"
1019+
# unsigned `sentry-trace`: add it.
1020+
assert "sentry-trace" in headers
9361021

9371022

9381023
@pytest.mark.asyncio

0 commit comments

Comments
 (0)