From e56b7856f322ad1625604f3d730d8205cd6bdcff Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Thu, 8 Jan 2026 15:19:15 +0100 Subject: [PATCH 1/2] Avoid duplicate headers when injecting on java.net http client --- .../HttpHeadersInstrumentation.java | 4 ++- .../httpclient/CaseInsensitiveKey.java | 36 +++++++++++++++++++ .../httpclient/HeadersAdvice.java | 17 +++++++-- .../httpclient/HttpHeadersInjectAdapter.java | 13 +++---- .../httpclient/JavaHttpClientTest.groovy | 19 +++++++++- 5 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java index 82b802d70a8..8c9286aab77 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java @@ -48,7 +48,9 @@ public ElementMatcher hierarchyMatcher() { @Override public String[] helperClassNames() { return new String[] { - packageName + ".HttpHeadersInjectAdapter", packageName + ".JavaNetClientDecorator", + packageName + ".CaseInsensitiveKey", + packageName + ".HttpHeadersInjectAdapter", + packageName + ".JavaNetClientDecorator", }; } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java new file mode 100644 index 00000000000..38bc7ae773a --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java @@ -0,0 +1,36 @@ +package datadog.trace.instrumentation.httpclient; + +import java.util.Locale; +import java.util.Objects; + +/** + * A class usable as key in a Set or Map that matches case-insensitively but still contains the + * original value. + */ +public final class CaseInsensitiveKey { + private final String value; + private final String lowercase; + + public CaseInsensitiveKey(final String value) { + this.value = value; + this.lowercase = value != null ? value.toLowerCase(Locale.ROOT) : null; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CaseInsensitiveKey)) { + return false; + } + CaseInsensitiveKey that = (CaseInsensitiveKey) o; + return Objects.equals(lowercase, that.lowercase); + } + + @Override + public int hashCode() { + return Objects.hashCode(lowercase); + } +} diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java index f3ed96ca617..4648400e6c9 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java @@ -6,7 +6,7 @@ import static datadog.trace.instrumentation.httpclient.JavaNetClientDecorator.DECORATE; import java.net.http.HttpHeaders; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import net.bytebuddy.asm.Advice; @@ -14,8 +14,19 @@ public class HeadersAdvice { @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit(@Advice.Return(readOnly = false) HttpHeaders headers) { - final Map> headerMap = new HashMap<>(headers.map()); + final Map> headerMap = new LinkedHashMap<>(); + // Note: we don't want to modify the case of the current headers + // However adding duplicate keys will throw an IllegalArgumentException so we need to dedupe + // case insensitively + for (final Map.Entry> entry : headers.map().entrySet()) { + headerMap.put(new CaseInsensitiveKey(entry.getKey()), entry.getValue()); + } DECORATE.injectContext(getCurrentContext(), headerMap, SETTER); - headers = HttpHeaders.of(headerMap, KEEP); + // convert back + final Map> finalMap = new LinkedHashMap<>(); + for (final Map.Entry> entry : headerMap.entrySet()) { + finalMap.put(entry.getKey().getValue(), entry.getValue()); + } + headers = HttpHeaders.of(finalMap, KEEP); } } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java index c9e95ca76c5..dbf770d4ee5 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java @@ -6,17 +6,18 @@ import java.util.Map; import java.util.function.BiPredicate; -public class HttpHeadersInjectAdapter implements CarrierSetter>> { +public class HttpHeadersInjectAdapter + implements CarrierSetter>> { public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); public static final BiPredicate KEEP = HttpHeadersInjectAdapter::keep; - @Override - public void set(final Map> carrier, final String key, final String value) { - carrier.put(key, Collections.singletonList(value)); - } - public static boolean keep(String key, String value) { return true; } + + @Override + public void set(Map> carrier, String key, String value) { + carrier.put(new CaseInsensitiveKey(key), Collections.singletonList(value)); + } } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JavaHttpClientTest.groovy b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JavaHttpClientTest.groovy index 66813e4757e..d53b1ffba6c 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JavaHttpClientTest.groovy +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JavaHttpClientTest.groovy @@ -1,8 +1,9 @@ package datadog.trace.instrumentation.httpclient -import datadog.trace.agent.test.base.HttpClientTest + import static datadog.trace.instrumentation.httpclient.JavaNetClientDecorator.DECORATE +import datadog.trace.agent.test.base.HttpClientTest import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse @@ -42,6 +43,22 @@ abstract class JavaHttpClientTest extends HttpClientTest { boolean testRedirects() { false } + + def 'should not inject duplicate headers'() { + when: + def status = doRequest("GET", server.address.resolve("/success"), + // our codec inject names all lowercase + ["X-Datadog-Trace-ID": "0"]) + + then: + status == 200 + assertTraces(2) { + trace(size(1)) { + clientSpan(it, null) + } + server.distributedRequestTrace(it, trace(0).last()) + } + } } class JavaHttpClientV0Test extends JavaHttpClientTest { From ea116a8c71ddd896fc27d49f1f7b6bd695985f1d Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Thu, 8 Jan 2026 16:26:52 +0100 Subject: [PATCH 2/2] Use TreeMap --- .../HttpHeadersInstrumentation.java | 4 +-- .../httpclient/CaseInsensitiveKey.java | 36 ------------------- .../httpclient/HeadersAdvice.java | 19 ++++------ .../httpclient/HttpHeadersInjectAdapter.java | 13 ++++--- 4 files changed, 13 insertions(+), 59 deletions(-) delete mode 100644 dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java index 8c9286aab77..82b802d70a8 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/HttpHeadersInstrumentation.java @@ -48,9 +48,7 @@ public ElementMatcher hierarchyMatcher() { @Override public String[] helperClassNames() { return new String[] { - packageName + ".CaseInsensitiveKey", - packageName + ".HttpHeadersInjectAdapter", - packageName + ".JavaNetClientDecorator", + packageName + ".HttpHeadersInjectAdapter", packageName + ".JavaNetClientDecorator", }; } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java deleted file mode 100644 index 38bc7ae773a..00000000000 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/CaseInsensitiveKey.java +++ /dev/null @@ -1,36 +0,0 @@ -package datadog.trace.instrumentation.httpclient; - -import java.util.Locale; -import java.util.Objects; - -/** - * A class usable as key in a Set or Map that matches case-insensitively but still contains the - * original value. - */ -public final class CaseInsensitiveKey { - private final String value; - private final String lowercase; - - public CaseInsensitiveKey(final String value) { - this.value = value; - this.lowercase = value != null ? value.toLowerCase(Locale.ROOT) : null; - } - - public String getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof CaseInsensitiveKey)) { - return false; - } - CaseInsensitiveKey that = (CaseInsensitiveKey) o; - return Objects.equals(lowercase, that.lowercase); - } - - @Override - public int hashCode() { - return Objects.hashCode(lowercase); - } -} diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java index 4648400e6c9..e7728a1c5ad 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HeadersAdvice.java @@ -4,29 +4,22 @@ import static datadog.trace.instrumentation.httpclient.HttpHeadersInjectAdapter.KEEP; import static datadog.trace.instrumentation.httpclient.HttpHeadersInjectAdapter.SETTER; import static datadog.trace.instrumentation.httpclient.JavaNetClientDecorator.DECORATE; +import static java.lang.String.CASE_INSENSITIVE_ORDER; import java.net.http.HttpHeaders; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import net.bytebuddy.asm.Advice; public class HeadersAdvice { @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit(@Advice.Return(readOnly = false) HttpHeaders headers) { - final Map> headerMap = new LinkedHashMap<>(); - // Note: we don't want to modify the case of the current headers - // However adding duplicate keys will throw an IllegalArgumentException so we need to dedupe + // Note: adding duplicate keys will throw an IllegalArgumentException so we need to dedupe // case insensitively - for (final Map.Entry> entry : headers.map().entrySet()) { - headerMap.put(new CaseInsensitiveKey(entry.getKey()), entry.getValue()); - } + final Map> headerMap = new TreeMap<>(CASE_INSENSITIVE_ORDER); + headerMap.putAll(headers.map()); DECORATE.injectContext(getCurrentContext(), headerMap, SETTER); - // convert back - final Map> finalMap = new LinkedHashMap<>(); - for (final Map.Entry> entry : headerMap.entrySet()) { - finalMap.put(entry.getKey().getValue(), entry.getValue()); - } - headers = HttpHeaders.of(finalMap, KEEP); + headers = HttpHeaders.of(headerMap, KEEP); } } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java index dbf770d4ee5..c9e95ca76c5 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java11/datadog/trace/instrumentation/httpclient/HttpHeadersInjectAdapter.java @@ -6,18 +6,17 @@ import java.util.Map; import java.util.function.BiPredicate; -public class HttpHeadersInjectAdapter - implements CarrierSetter>> { +public class HttpHeadersInjectAdapter implements CarrierSetter>> { public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); public static final BiPredicate KEEP = HttpHeadersInjectAdapter::keep; - public static boolean keep(String key, String value) { - return true; + @Override + public void set(final Map> carrier, final String key, final String value) { + carrier.put(key, Collections.singletonList(value)); } - @Override - public void set(Map> carrier, String key, String value) { - carrier.put(new CaseInsensitiveKey(key), Collections.singletonList(value)); + public static boolean keep(String key, String value) { + return true; } }