From 1a31c9b28d2c331bd0ec9956d15051e3b20671df Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 3 Mar 2026 12:27:21 +0100 Subject: [PATCH 1/5] fix(celery): Propagate user-set headers --- sentry_sdk/integrations/celery/__init__.py | 6 +++ tests/integrations/celery/test_celery.py | 56 ++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/sentry_sdk/integrations/celery/__init__.py b/sentry_sdk/integrations/celery/__init__.py index 1b1eb4f380..fe832f159e 100644 --- a/sentry_sdk/integrations/celery/__init__.py +++ b/sentry_sdk/integrations/celery/__init__.py @@ -231,6 +231,12 @@ def _update_celery_task_headers( if key.startswith("sentry-"): updated_headers["headers"][key] = value + # Preserve user-provided custom headers in the inner "headers" dict + # so they survive to task.request.headers on the worker (celery#4875). + for key, value in original_headers.items(): + if key != "headers" and key not in updated_headers["headers"]: + updated_headers["headers"][key] = value + return updated_headers diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index b8fc2bb3e8..77699e4958 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -847,6 +847,62 @@ def test_send_task_wrapped( assert span["trace_id"] == kwargs["headers"]["sentry-trace"].split("-")[0] +def test_user_custom_headers_accessible_in_task(init_celery): + """ + Regression test for https://github.com/getsentry/sentry-python/issues/5566 + + User-provided custom headers passed to apply_async() must be accessible + via task.request.headers on the worker side. + """ + celery = init_celery(traces_sample_rate=1.0) + + @celery.task(name="custom_headers_task", bind=True) + def custom_headers_task(self): + return dict(self.request.headers or {}) + + custom_headers = { + "my_custom_key": "my_value", + "correlation_id": "abc-123", + "tenant_id": "tenant-42", + } + + with start_transaction(name="test"): + result = custom_headers_task.apply_async(headers=custom_headers) + + received_headers = result.get() + for key, value in custom_headers.items(): + assert received_headers.get(key) == value, ( + f"Custom header {key!r} not found in task.request.headers" + ) + + +def test_user_custom_headers_do_not_overwrite_sentry_headers(init_celery): + """ + If a user passes a header with a key that collides with a Sentry header, + the Sentry-generated value must take precedence. + """ + celery = init_celery(traces_sample_rate=1.0) + + @celery.task(name="headers_precedence_task", bind=True) + def headers_precedence_task(self): + return dict(self.request.headers or {}) + + with start_transaction(name="test"): + result = headers_precedence_task.apply_async( + headers={ + "sentry-trace": "user-supplied-value", + "my_custom_key": "my_value", + }, + ) + + received_headers = result.get() + # Sentry's own sentry-trace must NOT be the user-supplied value + assert received_headers.get("sentry-trace") != "user-supplied-value" + assert "sentry-trace" in received_headers + # User custom header is still preserved + assert received_headers.get("my_custom_key") == "my_value" + + @pytest.mark.skip(reason="placeholder so that forked test does not come last") def test_placeholder(): """Forked tests must not come last in the module. From 9e59ee5e9981daeeff568140b3d1086abdc4b1be Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 3 Mar 2026 12:43:39 +0100 Subject: [PATCH 2/5] Update sentry_sdk/integrations/celery/__init__.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- sentry_sdk/integrations/celery/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/celery/__init__.py b/sentry_sdk/integrations/celery/__init__.py index fe832f159e..0b6fd934a7 100644 --- a/sentry_sdk/integrations/celery/__init__.py +++ b/sentry_sdk/integrations/celery/__init__.py @@ -233,9 +233,12 @@ def _update_celery_task_headers( # Preserve user-provided custom headers in the inner "headers" dict # so they survive to task.request.headers on the worker (celery#4875). - for key, value in original_headers.items(): - if key != "headers" and key not in updated_headers["headers"]: - updated_headers["headers"][key] = value + # Preserve user-provided custom headers in the inner "headers" dict + # so they survive to task.request.headers on the worker (celery#4875). + inner_headers = updated_headers.setdefault("headers", {}) + for key, value in original_headers.items(): + if key != "headers" and key not in inner_headers: + inner_headers[key] = value return updated_headers From 73a96b233aa4406c1b9c6c3390d9a4e9743d759a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 3 Mar 2026 12:46:36 +0100 Subject: [PATCH 3/5] . --- sentry_sdk/integrations/celery/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry_sdk/integrations/celery/__init__.py b/sentry_sdk/integrations/celery/__init__.py index 0b6fd934a7..3f113147a0 100644 --- a/sentry_sdk/integrations/celery/__init__.py +++ b/sentry_sdk/integrations/celery/__init__.py @@ -231,8 +231,6 @@ def _update_celery_task_headers( if key.startswith("sentry-"): updated_headers["headers"][key] = value - # Preserve user-provided custom headers in the inner "headers" dict - # so they survive to task.request.headers on the worker (celery#4875). # Preserve user-provided custom headers in the inner "headers" dict # so they survive to task.request.headers on the worker (celery#4875). inner_headers = updated_headers.setdefault("headers", {}) From 0187bfb724987e1a49270993febfe49bbd5b9120 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 3 Mar 2026 12:51:40 +0100 Subject: [PATCH 4/5] . --- tests/integrations/celery/test_celery.py | 27 ------------------------ 1 file changed, 27 deletions(-) diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index 77699e4958..42ae6ea14f 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -876,33 +876,6 @@ def custom_headers_task(self): ) -def test_user_custom_headers_do_not_overwrite_sentry_headers(init_celery): - """ - If a user passes a header with a key that collides with a Sentry header, - the Sentry-generated value must take precedence. - """ - celery = init_celery(traces_sample_rate=1.0) - - @celery.task(name="headers_precedence_task", bind=True) - def headers_precedence_task(self): - return dict(self.request.headers or {}) - - with start_transaction(name="test"): - result = headers_precedence_task.apply_async( - headers={ - "sentry-trace": "user-supplied-value", - "my_custom_key": "my_value", - }, - ) - - received_headers = result.get() - # Sentry's own sentry-trace must NOT be the user-supplied value - assert received_headers.get("sentry-trace") != "user-supplied-value" - assert "sentry-trace" in received_headers - # User custom header is still preserved - assert received_headers.get("my_custom_key") == "my_value" - - @pytest.mark.skip(reason="placeholder so that forked test does not come last") def test_placeholder(): """Forked tests must not come last in the module. From fde36c9ebbfb69e3135e10161688cf4fcd4d015e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 3 Mar 2026 12:57:22 +0100 Subject: [PATCH 5/5] . --- sentry_sdk/integrations/celery/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/celery/__init__.py b/sentry_sdk/integrations/celery/__init__.py index 3f113147a0..fe832f159e 100644 --- a/sentry_sdk/integrations/celery/__init__.py +++ b/sentry_sdk/integrations/celery/__init__.py @@ -231,12 +231,11 @@ def _update_celery_task_headers( if key.startswith("sentry-"): updated_headers["headers"][key] = value - # Preserve user-provided custom headers in the inner "headers" dict - # so they survive to task.request.headers on the worker (celery#4875). - inner_headers = updated_headers.setdefault("headers", {}) - for key, value in original_headers.items(): - if key != "headers" and key not in inner_headers: - inner_headers[key] = value + # Preserve user-provided custom headers in the inner "headers" dict + # so they survive to task.request.headers on the worker (celery#4875). + for key, value in original_headers.items(): + if key != "headers" and key not in updated_headers["headers"]: + updated_headers["headers"][key] = value return updated_headers