From 959c2cc78608ddce5ec7757b9fead37ecdf479fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 17 Feb 2026 12:41:56 +0100 Subject: [PATCH 1/4] instrument inner requests in apache http client --- .../ApacheHttpClientInstrumentation.java | 52 ++++++++++++--- .../apachehttpclient5/HelperMethods.java | 65 +++++++++++++------ .../test/groovy/ApacheHttpClientTest.groovy | 52 +++++++++++++++ 3 files changed, 140 insertions(+), 29 deletions(-) diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java index 051b5305771..2920762a378 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java @@ -10,13 +10,17 @@ import datadog.appsec.api.blocking.BlockingException; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import java.util.Collections; +import java.util.Map; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.matcher.ElementMatcher; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.protocol.HttpContext; @@ -68,6 +72,11 @@ public String[] helperClassNames() { }; } + @Override + public Map contextStore() { + return Collections.singletonMap("org.apache.hc.core5.http.HttpRequest", "java.lang.Integer"); + } + @Override public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( @@ -117,9 +126,11 @@ public static class RequestAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static AgentScope methodEnter(@Advice.Argument(0) final ClassicHttpRequest request) { try { - return HelperMethods.doMethodEnter(request); + return HelperMethods.doMethodEnter( + InstrumentationContext.get(HttpRequest.class, Integer.class), request); } catch (BlockingException e) { - HelperMethods.onBlockingRequest(); + HelperMethods.onBlockingRequest( + InstrumentationContext.get(HttpRequest.class, Integer.class), request); // re-throw blocking exceptions throw e; } @@ -127,10 +138,16 @@ public static AgentScope methodEnter(@Advice.Argument(0) final ClassicHttpReques @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( + @Advice.Argument(0) final ClassicHttpRequest request, @Advice.Enter final AgentScope scope, @Advice.Return final Object result, @Advice.Thrown final Throwable throwable) { - HelperMethods.doMethodExit(scope, result, throwable); + HelperMethods.doMethodExit( + InstrumentationContext.get(HttpRequest.class, Integer.class), + request, + scope, + result, + throwable); } } @@ -140,9 +157,11 @@ public static AgentScope methodEnter( @Advice.Argument(0) final HttpHost host, @Advice.Argument(1) final ClassicHttpRequest request) { try { - return HelperMethods.doMethodEnter(host, request); + return HelperMethods.doMethodEnter( + InstrumentationContext.get(HttpRequest.class, Integer.class), host, request); } catch (BlockingException e) { - HelperMethods.onBlockingRequest(); + HelperMethods.onBlockingRequest( + InstrumentationContext.get(HttpRequest.class, Integer.class), request); // re-throw blocking exceptions throw e; } @@ -150,10 +169,16 @@ public static AgentScope methodEnter( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( + @Advice.Argument(1) final ClassicHttpRequest request, @Advice.Enter final AgentScope scope, @Advice.Return final Object result, @Advice.Thrown final Throwable throwable) { - HelperMethods.doMethodExit(scope, result, throwable); + HelperMethods.doMethodExit( + InstrumentationContext.get(HttpRequest.class, Integer.class), + request, + scope, + result, + throwable); } } @@ -171,7 +196,9 @@ public static AgentScope methodEnter( readOnly = false) Object handler) { try { - final AgentScope scope = HelperMethods.doMethodEnter(host, request); + final AgentScope scope = + HelperMethods.doMethodEnter( + InstrumentationContext.get(HttpRequest.class, Integer.class), host, request); // Wrap the handler so we capture the status code if (null != scope && handler instanceof HttpClientResponseHandler) { handler = @@ -180,7 +207,8 @@ public static AgentScope methodEnter( } return scope; } catch (BlockingException e) { - HelperMethods.onBlockingRequest(); + HelperMethods.onBlockingRequest( + InstrumentationContext.get(HttpRequest.class, Integer.class), request); // re-throw blocking exceptions throw e; } @@ -188,10 +216,16 @@ public static AgentScope methodEnter( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( + @Advice.Argument(1) final ClassicHttpRequest request, @Advice.Enter final AgentScope scope, @Advice.Return final Object result, @Advice.Thrown final Throwable throwable) { - HelperMethods.doMethodExit(scope, result, throwable); + HelperMethods.doMethodExit( + InstrumentationContext.get(HttpRequest.class, Integer.class), + request, + scope, + result, + throwable); } } } diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java index e6187609433..9e90617cea9 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java @@ -7,33 +7,43 @@ import static datadog.trace.instrumentation.apachehttpclient5.ApacheHttpClientDecorator.HTTP_REQUEST; import static datadog.trace.instrumentation.apachehttpclient5.HttpHeadersInjectAdapter.SETTER; -import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.ContextStore; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; public class HelperMethods { - public static AgentScope doMethodEnter(final HttpRequest request) { - final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class); - if (callDepth > 0) { + + public static AgentScope doMethodEnter( + final ContextStore depthStore, final HttpRequest request) { + int depth = incrementDepth(depthStore, request); + if (depth > 1) { return null; } - return activateHttpSpan(request); } - public static AgentScope doMethodEnter(HttpHost host, HttpRequest request) { - final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class); - if (callDepth > 0) { + public static AgentScope doMethodEnter( + final ContextStore depthStore, + HttpHost host, + final HttpRequest request) { + int depth = incrementDepth(depthStore, request); + if (depth > 1) { return null; } - return activateHttpSpan(new HostAndRequestAsHttpUriRequest(host, request)); } + private static int incrementDepth( + final ContextStore depthStore, final HttpRequest request) { + Integer depth = depthStore.get(request); + depth = depth == null ? 1 : (depth + 1); + depthStore.put(request, depth); + return depth; + } + private static AgentScope activateHttpSpan(final HttpRequest request) { final AgentSpan span = startSpan(HTTP_REQUEST); final AgentScope scope = activateSpan(span); @@ -51,12 +61,16 @@ private static AgentScope activateHttpSpan(final HttpRequest request) { } public static void doMethodExit( - final AgentScope scope, final Object result, final Throwable throwable) { - if (scope == null) { - return; - } - final AgentSpan span = scope.span(); + final ContextStore depthStore, + final HttpRequest request, + final AgentScope scope, + final Object result, + final Throwable throwable) { try { + if (scope == null) { + return; + } + final AgentSpan span = scope.span(); if (result instanceof HttpResponse) { DECORATE.onResponse(span, (HttpResponse) result); } // else they probably provided a ResponseHandler. @@ -64,13 +78,24 @@ public static void doMethodExit( DECORATE.onError(span, throwable); DECORATE.beforeFinish(span); } finally { - scope.close(); - span.finish(); - CallDepthThreadLocalMap.reset(HttpClient.class); + if (scope != null) { + AgentSpan span = scope.span(); + scope.close(); + span.finish(); + } + Integer depth = depthStore.get(request); + if (depth != null && depth > 0) { + depthStore.put(request, depth - 1); + } } } - public static void onBlockingRequest() { - CallDepthThreadLocalMap.reset(HttpClient.class); + /** + * Cleans up state when a BlockingException is thrown from methodEnter. Since the exception + * unwinds the whole stack, we this request's depth to 0. + */ + public static void onBlockingRequest( + final ContextStore depthStore, final HttpRequest request) { + depthStore.put(request, 0); } } diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy index c1a27127857..7df62a15157 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy @@ -215,3 +215,55 @@ class ApacheClientResponseHandlerAllV0Test extends ApacheClientResponseHandlerAl class ApacheClientResponseHandlerAllV1ForkedTest extends ApacheClientResponseHandlerAll implements TestingGenericHttpNamingConventions.ClientV1 { } +/** + * Tests that HTTP calls made from within an exec interceptor (or other nested execute() context) + * using a different HttpClient instance are instrumented. See + * https://github.com/DataDog/dd-trace-java/issues/10383 + */ +@Timeout(5) +class ApacheClientNestedExecuteTest extends ApacheHttpClientTest { + + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new BasicClassicHttpRequest(method, uri) + } + + @Override + CloseableHttpResponse executeRequest(ClassicHttpRequest request, URI uri) { + return client.execute(request) + } + + def "nested execute from different client (e.g. interceptor token fetch) creates spans for both requests"() { + when: + def tokenUri = server.address.resolve("/success") + def mainUri = server.address.resolve("/success") + def innerCode = new int[1] + // Use a separate client for the inner request (as in issue: token client in interceptor) + def tokenClient = HttpClients.custom() + .setConnectionManager(new BasicHttpClientConnectionManager()) + .setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build()).build() + def response = client.execute(new BasicClassicHttpRequest("GET", mainUri), { r -> + innerCode[0] = tokenClient.execute(new BasicClassicHttpRequest("GET", tokenUri), { inner -> inner.code }) + return r + }) + tokenClient.close() + + then: + response != null + innerCode[0] == 200 + assertTraces(3) { + sortSpansByStart() + trace(size(2)) { + sortSpansByStart() + // Main request starts first, then token request (nested) - order by start time + clientSpan(it, null, "GET", false, false, mainUri) + clientSpan(it, span(0), "GET", false, false, tokenUri) + } + server.distributedRequestTrace(it, trace(0)[0]) + server.distributedRequestTrace(it, trace(0)[1]) + } + } +} + From c7b8602ab8ce343f21edc336462a889ec61724cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 17 Feb 2026 14:41:05 +0100 Subject: [PATCH 2/4] use boolean instead of int --- .../ApacheHttpClientInstrumentation.java | 76 +++++-------------- .../apachehttpclient5/HelperMethods.java | 64 ++++++---------- 2 files changed, 46 insertions(+), 94 deletions(-) diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java index 2920762a378..9df8d93c7d0 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientInstrumentation.java @@ -7,7 +7,6 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import com.google.auto.service.AutoService; -import datadog.appsec.api.blocking.BlockingException; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.bootstrap.InstrumentationContext; @@ -20,7 +19,6 @@ import net.bytebuddy.matcher.ElementMatcher; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.protocol.HttpContext; @@ -74,7 +72,11 @@ public String[] helperClassNames() { @Override public Map contextStore() { - return Collections.singletonMap("org.apache.hc.core5.http.HttpRequest", "java.lang.Integer"); + // used to mark when a request has been instrumented. + // We don't count depth like we usually do, because sub-requests can be spawned and need to be + // traced + return Collections.singletonMap( + "org.apache.hc.core5.http.ClassicHttpRequest", "java.lang.Boolean"); } @Override @@ -125,15 +127,8 @@ public void methodAdvice(MethodTransformer transformer) { public static class RequestAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static AgentScope methodEnter(@Advice.Argument(0) final ClassicHttpRequest request) { - try { - return HelperMethods.doMethodEnter( - InstrumentationContext.get(HttpRequest.class, Integer.class), request); - } catch (BlockingException e) { - HelperMethods.onBlockingRequest( - InstrumentationContext.get(HttpRequest.class, Integer.class), request); - // re-throw blocking exceptions - throw e; - } + return HelperMethods.doMethodEnter( + InstrumentationContext.get(ClassicHttpRequest.class, Boolean.class), request); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) @@ -142,12 +137,7 @@ public static void methodExit( @Advice.Enter final AgentScope scope, @Advice.Return final Object result, @Advice.Thrown final Throwable throwable) { - HelperMethods.doMethodExit( - InstrumentationContext.get(HttpRequest.class, Integer.class), - request, - scope, - result, - throwable); + HelperMethods.doMethodExit(scope, result, throwable); } } @@ -156,15 +146,8 @@ public static class HostRequestAdvice { public static AgentScope methodEnter( @Advice.Argument(0) final HttpHost host, @Advice.Argument(1) final ClassicHttpRequest request) { - try { - return HelperMethods.doMethodEnter( - InstrumentationContext.get(HttpRequest.class, Integer.class), host, request); - } catch (BlockingException e) { - HelperMethods.onBlockingRequest( - InstrumentationContext.get(HttpRequest.class, Integer.class), request); - // re-throw blocking exceptions - throw e; - } + return HelperMethods.doMethodEnter( + InstrumentationContext.get(ClassicHttpRequest.class, Boolean.class), host, request); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) @@ -173,12 +156,7 @@ public static void methodExit( @Advice.Enter final AgentScope scope, @Advice.Return final Object result, @Advice.Thrown final Throwable throwable) { - HelperMethods.doMethodExit( - InstrumentationContext.get(HttpRequest.class, Integer.class), - request, - scope, - result, - throwable); + HelperMethods.doMethodExit(scope, result, throwable); } } @@ -195,23 +173,16 @@ public static AgentScope methodEnter( typing = Assigner.Typing.DYNAMIC, readOnly = false) Object handler) { - try { - final AgentScope scope = - HelperMethods.doMethodEnter( - InstrumentationContext.get(HttpRequest.class, Integer.class), host, request); - // Wrap the handler so we capture the status code - if (null != scope && handler instanceof HttpClientResponseHandler) { - handler = - new WrappingStatusSettingResponseHandler( - scope.span(), (HttpClientResponseHandler) handler); - } - return scope; - } catch (BlockingException e) { - HelperMethods.onBlockingRequest( - InstrumentationContext.get(HttpRequest.class, Integer.class), request); - // re-throw blocking exceptions - throw e; + final AgentScope scope = + HelperMethods.doMethodEnter( + InstrumentationContext.get(ClassicHttpRequest.class, Boolean.class), host, request); + // Wrap the handler so we capture the status code + if (null != scope && handler instanceof HttpClientResponseHandler) { + handler = + new WrappingStatusSettingResponseHandler( + scope.span(), (HttpClientResponseHandler) handler); } + return scope; } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) @@ -220,12 +191,7 @@ public static void methodExit( @Advice.Enter final AgentScope scope, @Advice.Return final Object result, @Advice.Thrown final Throwable throwable) { - HelperMethods.doMethodExit( - InstrumentationContext.get(HttpRequest.class, Integer.class), - request, - scope, - result, - throwable); + HelperMethods.doMethodExit(scope, result, throwable); } } } diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java index 9e90617cea9..e39e1889271 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java @@ -10,6 +10,7 @@ import datadog.trace.bootstrap.ContextStore; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; @@ -17,31 +18,35 @@ public class HelperMethods { public static AgentScope doMethodEnter( - final ContextStore depthStore, final HttpRequest request) { - int depth = incrementDepth(depthStore, request); - if (depth > 1) { + final ContextStore instrumentationMarker, + final ClassicHttpRequest request) { + if (testAndSet(instrumentationMarker, request)) { return null; } return activateHttpSpan(request); } public static AgentScope doMethodEnter( - final ContextStore depthStore, + final ContextStore instrumentationMarker, HttpHost host, - final HttpRequest request) { - int depth = incrementDepth(depthStore, request); - if (depth > 1) { + final ClassicHttpRequest request) { + if (testAndSet(instrumentationMarker, request)) { return null; } return activateHttpSpan(new HostAndRequestAsHttpUriRequest(host, request)); } - private static int incrementDepth( - final ContextStore depthStore, final HttpRequest request) { - Integer depth = depthStore.get(request); - depth = depth == null ? 1 : (depth + 1); - depthStore.put(request, depth); - return depth; + // checks current value in context store, + // and ensures + private static boolean testAndSet( + final ContextStore instrumentationMarker, + final ClassicHttpRequest request) { + Boolean instrumented = instrumentationMarker.get(request); + if (instrumented == Boolean.TRUE) { + return true; + } + instrumentationMarker.put(request, Boolean.TRUE); + return false; } private static AgentScope activateHttpSpan(final HttpRequest request) { @@ -61,15 +66,11 @@ private static AgentScope activateHttpSpan(final HttpRequest request) { } public static void doMethodExit( - final ContextStore depthStore, - final HttpRequest request, - final AgentScope scope, - final Object result, - final Throwable throwable) { + final AgentScope scope, final Object result, final Throwable throwable) { + if (scope == null) { + return; + } try { - if (scope == null) { - return; - } final AgentSpan span = scope.span(); if (result instanceof HttpResponse) { DECORATE.onResponse(span, (HttpResponse) result); @@ -78,24 +79,9 @@ public static void doMethodExit( DECORATE.onError(span, throwable); DECORATE.beforeFinish(span); } finally { - if (scope != null) { - AgentSpan span = scope.span(); - scope.close(); - span.finish(); - } - Integer depth = depthStore.get(request); - if (depth != null && depth > 0) { - depthStore.put(request, depth - 1); - } + AgentSpan span = scope.span(); + scope.close(); + span.finish(); } } - - /** - * Cleans up state when a BlockingException is thrown from methodEnter. Since the exception - * unwinds the whole stack, we this request's depth to 0. - */ - public static void onBlockingRequest( - final ContextStore depthStore, final HttpRequest request) { - depthStore.put(request, 0); - } } From 9c4389532ae073d5c0e44fb46882899d6bf1cde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 17 Feb 2026 15:52:26 +0100 Subject: [PATCH 3/4] make the test look like the repro code the user gave --- .../test/groovy/ApacheHttpClientTest.groovy | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy index 7df62a15157..13a89e35f59 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/test/groovy/ApacheHttpClientTest.groovy @@ -216,9 +216,9 @@ class ApacheClientResponseHandlerAllV1ForkedTest extends ApacheClientResponseHan } /** - * Tests that HTTP calls made from within an exec interceptor (or other nested execute() context) - * using a different HttpClient instance are instrumented. See - * https://github.com/DataDog/dd-trace-java/issues/10383 + * Tests that HTTP calls made from within an ExecChainHandler (exec interceptor) are instrumented. + * Reproduces the scenario from https://github.com/DataDog/dd-trace-java/issues/10383: an + * interceptor fetches a token via a separate HttpClient, adds it as a header, then proceeds. */ @Timeout(5) class ApacheClientNestedExecuteTest extends ApacheHttpClientTest { @@ -233,37 +233,54 @@ class ApacheClientNestedExecuteTest extends ApacheHttpClientTest - innerCode[0] = tokenClient.execute(new BasicClassicHttpRequest("GET", tokenUri), { inner -> inner.code }) - return r - }) - tokenClient.close() + .setDefaultRequestConfig(requestConfig) + .build() + def tokenUriFinal = tokenUri + def clientWithInterceptor = HttpClients.custom() + .setConnectionManager(new BasicHttpClientConnectionManager()) + .setDefaultRequestConfig(requestConfig) + .addExecInterceptorLast("token", { request, scope, chain -> + String token = tokenClient.execute( + new BasicClassicHttpRequest("GET", tokenUriFinal), { resp -> + String.valueOf(resp.code) + } + ) + request.addHeader(new BasicHeader("x-token", token)) + return chain.proceed(request, scope) + }) + .build() + def response = clientWithInterceptor.execute( + new BasicClassicHttpRequest("GET", mainUri), { r -> + r + } + ) then: response != null - innerCode[0] == 200 assertTraces(3) { sortSpansByStart() trace(size(2)) { sortSpansByStart() - // Main request starts first, then token request (nested) - order by start time - clientSpan(it, null, "GET", false, false, mainUri) - clientSpan(it, span(0), "GET", false, false, tokenUri) + // Token request runs first (inside interceptor), then main request + clientSpan(it, null, "GET", false, false, tokenUri) + clientSpan(it, span(0), "GET", false, false, mainUri) } server.distributedRequestTrace(it, trace(0)[0]) server.distributedRequestTrace(it, trace(0)[1]) } + + cleanup: + tokenClient?.close() + clientWithInterceptor?.close() } } From 8d08af8980596edc29e7835c3bda65db75c557d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 18 Feb 2026 13:17:58 +0100 Subject: [PATCH 4/4] fixes --- .../apache-httpclient/apache-httpclient-5.0/build.gradle | 2 +- .../instrumentation/apachehttpclient5/HelperMethods.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/build.gradle b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/build.gradle index 0bcee37c285..5c56b57bd45 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/build.gradle +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/build.gradle @@ -14,7 +14,7 @@ addTestSuiteForDir('latestDepTest', 'test') dependencies { compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0' - testImplementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0' + testImplementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.2' // we need this fix https://github.com/apache/httpcomponents-client/commit/a22d889807eb74a0d4b8dd8d6360802a391a6eb3 latestDepTestImplementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '+' } diff --git a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java index e39e1889271..c962ada1455 100644 --- a/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java +++ b/dd-java-agent/instrumentation/apache-httpclient/apache-httpclient-5.0/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HelperMethods.java @@ -37,10 +37,14 @@ public static AgentScope doMethodEnter( } // checks current value in context store, - // and ensures + // and ensures it's set to true when this method exists private static boolean testAndSet( final ContextStore instrumentationMarker, final ClassicHttpRequest request) { + if (request == null) { + // we probably don't want to instrument a call with a null request ? + return true; + } Boolean instrumented = instrumentationMarker.get(request); if (instrumented == Boolean.TRUE) { return true;