From 49edadd87972ac627778207bbbc8a584e8cdfe3d Mon Sep 17 00:00:00 2001 From: "paul.laborde" Date: Wed, 5 Aug 2026 08:01:57 +0000 Subject: [PATCH 1/2] fix(llmobs): parse additional headers with an explicit separator to support values containing spaces _DD_TRACE_WRITER_ADDITIONAL_HEADERS values like "Authorization:Bearer " (space in the value, no comma anywhere) were incorrectly whitespace-split by parse_tags_str's auto-detection, corrupting the header. parse_tags_str now accepts an optional explicit separator, and the LLMObs writer passes "," to avoid the whitespace fallback. Environment: Datadog workspace Co-Authored-By: Claude Sonnet 5 --- ddtrace/internal/utils/formats.py | 7 ++-- ddtrace/llmobs/_writer.py | 5 ++- ...ers-whitespace-split-53067051dd9a93f2.yaml | 6 ++++ .../test_llmobs_span_agentless_writer.py | 32 +++++++++++++++++++ tests/tracer/test_utils.py | 14 ++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml diff --git a/ddtrace/internal/utils/formats.py b/ddtrace/internal/utils/formats.py index 1fd5f007e4a..02dd5381b06 100644 --- a/ddtrace/internal/utils/formats.py +++ b/ddtrace/internal/utils/formats.py @@ -63,7 +63,7 @@ def asbool(value: Union[str, bool, None]) -> bool: return value.lower() in ("true", "1") -def parse_tags_str(tags_str: Optional[str]) -> dict[str, str]: +def parse_tags_str(tags_str: Optional[str], sep: Optional[str] = None) -> dict[str, str]: """ Parses a string containing key-value pairs and returns a dictionary. Key-value pairs are delimited by ':', and pairs are separated by whitespace, comma, OR BOTH. @@ -71,13 +71,16 @@ def parse_tags_str(tags_str: Optional[str]) -> dict[str, str]: This implementation aligns with the way tags are parsed by the Agent and other Datadog SDKs :param tags_str: A string of the above form to parse tags from. + :param sep: An explicit pair separator to use instead of auto-detecting one. Callers whose + values may themselves contain whitespace (and no comma) should pass "," here to avoid an + incorrect whitespace-based split. :return: A dict containing the tags that were parsed. """ res: dict[str, str] = {} if not tags_str: return res # falling back to comma as separator - sep = "," if "," in tags_str else " " + sep = sep if sep is not None else ("," if "," in tags_str else " ") for tag in tags_str.split(sep): tag = tag.strip() diff --git a/ddtrace/llmobs/_writer.py b/ddtrace/llmobs/_writer.py index 0783ca90e76..6909008915e 100644 --- a/ddtrace/llmobs/_writer.py +++ b/ddtrace/llmobs/_writer.py @@ -216,7 +216,10 @@ def __init__( self._headers[EVP_SUBDOMAIN_HEADER_NAME] = self.EVP_SUBDOMAIN_HEADER_VALUE additional_header_str = env.get("_DD_TRACE_WRITER_ADDITIONAL_HEADERS", "") if additional_header_str: - self._headers.update(parse_tags_str(additional_header_str)) + # Explicit comma separator: header values (e.g. "Bearer ") may contain a space + # with no comma anywhere in the string, which would otherwise fall back to a whitespace + # split and corrupt the value. + self._headers.update(parse_tags_str(additional_header_str, sep=",")) self._send_payload_with_retry = fibonacci_backoff_with_jitter( attempts=self.RETRY_ATTEMPTS, diff --git a/releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml b/releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml new file mode 100644 index 00000000000..e2254d5c6fd --- /dev/null +++ b/releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + LLM Observability: fixes an issue where a ``_DD_TRACE_WRITER_ADDITIONAL_HEADERS`` header value + containing whitespace (for example ``Authorization:Bearer ``) was incorrectly split on + whitespace instead of being kept as a single value, when the overall string contained no comma. diff --git a/tests/llmobs/test_llmobs_span_agentless_writer.py b/tests/llmobs/test_llmobs_span_agentless_writer.py index bcc06ef54e4..907a9964ebb 100644 --- a/tests/llmobs/test_llmobs_span_agentless_writer.py +++ b/tests/llmobs/test_llmobs_span_agentless_writer.py @@ -37,6 +37,38 @@ def test_additional_headers(mock_writer_logs): assert llmobs_span_writer._headers["Authorization"] == "Bearer-custom-token" +@pytest.mark.parametrize( + "additional_headers,expected_headers", + [ + # No comma: would wrongly fall back to a whitespace split. + ( + "Authorization:Bearer custom-token", + {"Authorization": "Bearer custom-token"}, + ), + # Trailing comma. + ( + "Authorization:Bearer custom-token,", + {"Authorization": "Bearer custom-token"}, + ), + # Multiple real-world headers, two with spaces in their value. + ( + 'Authorization:Bearer abc123xyz,User-Agent:"Datadog Tracer",X-Request-Id:req-12345', + { + "Authorization": "Bearer abc123xyz", + "User-Agent": '"Datadog Tracer"', + "X-Request-Id": "req-12345", + }, + ), + ], +) +def test_additional_headers_with_spaces(mock_writer_logs, additional_headers, expected_headers): + assert expected_headers + with mock.patch.dict(os.environ, {"_DD_TRACE_WRITER_ADDITIONAL_HEADERS": additional_headers}): + llmobs_span_writer = LLMObsSpanWriter(1, 1, is_agentless=True, _site=DD_SITE, _api_key=DD_API_KEY) + for key, value in expected_headers.items(): + assert llmobs_span_writer._headers[key] == value + + def test_no_additional_headers_by_default(mock_writer_logs): llmobs_span_writer = LLMObsSpanWriter(1, 1, is_agentless=True, _site=DD_SITE, _api_key=DD_API_KEY) assert "Authorization" not in llmobs_span_writer._headers diff --git a/tests/tracer/test_utils.py b/tests/tracer/test_utils.py index 06643c78a7c..486269dd290 100644 --- a/tests/tracer/test_utils.py +++ b/tests/tracer/test_utils.py @@ -67,6 +67,20 @@ def test_parse_env_tags(tag_str, expected_tags): assert parse_tags_str(tag_str) == expected_tags, tag_str +@pytest.mark.parametrize( + "tag_str,sep,expected_tags", + [ + # No comma anywhere: auto-detection would fall back to whitespace and split "Bearer ". + # An explicit "," separator keeps it as a single value. + ("Authorization:Bearer abc123xyz", ",", {"Authorization": "Bearer abc123xyz"}), + # Explicit " " separator forces a whitespace split even though a comma is present. + ("a:b,c bKey:bVal", " ", {"a": "b,c", "bKey": "bVal"}), + ], +) +def test_parse_env_tags_explicit_sep(tag_str, sep, expected_tags): + assert parse_tags_str(tag_str, sep=sep) == expected_tags, tag_str + + @pytest.mark.parametrize( "key,value,expected", [ From 4c9b14522e04765130ee61cd0e516dc9621f2a8d Mon Sep 17 00:00:00 2001 From: "paul.laborde" Date: Wed, 12 Aug 2026 12:51:43 +0000 Subject: [PATCH 2/2] docs(llmobs): reword release note to lead with impact, per review feedback Environment: Datadog workspace Co-Authored-By: Claude Sonnet 5 --- ...dditional-headers-whitespace-split-53067051dd9a93f2.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml b/releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml index e2254d5c6fd..409fef2b3d2 100644 --- a/releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml +++ b/releasenotes/notes/fix-additional-headers-whitespace-split-53067051dd9a93f2.yaml @@ -1,6 +1,6 @@ --- fixes: - | - LLM Observability: fixes an issue where a ``_DD_TRACE_WRITER_ADDITIONAL_HEADERS`` header value - containing whitespace (for example ``Authorization:Bearer ``) was incorrectly split on - whitespace instead of being kept as a single value, when the overall string contained no comma. + LLM Observability: fixes an issue where ``_DD_TRACE_WRITER_ADDITIONAL_HEADERS`` header values + containing a whitespace (for example ``Bearer ``) were incorrectly parsed, resulting in + a truncated header value that caused LLM Observability spans to be dropped.