From 281e32ff6e541af3ef445ee38966a5e27c6ad3db Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 3 Mar 2026 12:06:31 +0100 Subject: [PATCH 1/2] Fix missing http.response.headers.content-type on blocking responses for Netty --- .../appsec/gateway/AppSecRequestContext.java | 5 +++ .../datadog/appsec/gateway/GatewayBridge.java | 6 ++- .../AppSecRequestContextSpecification.groovy | 31 +++++++++++++ .../gateway/GatewayBridgeSpecification.groovy | 44 +++++++++++++++++++ .../akkahttp/DatadogAsyncHandlerWrapper.java | 6 +-- .../appsec/BlockingResponseHelper.java | 12 ++++- .../server/BlockingResponseHandler.java | 17 +++++++ .../server/BlockingResponseHandler.java | 17 +++++++ 8 files changed, 132 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java index d5df33efa7d..caeb59cc785 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java @@ -529,6 +529,11 @@ public boolean isFinishedResponseHeaders() { return finishedResponseHeaders; } + void clearResponseHeadersForBlocking() { + responseHeaders.clear(); + finishedResponseHeaders = false; + } + Map> getResponseHeaders() { return responseHeaders; } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java index f95e6dfaf2c..b55d0e9bfc9 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java @@ -657,7 +657,11 @@ private Flow onResponseHeaderDone(RequestContext ctx_) { return NoopFlow.INSTANCE; } ctx.finishResponseHeaders(); - return maybePublishResponseData(ctx); + Flow flow = maybePublishResponseData(ctx); + if (flow.getAction() instanceof Flow.Action.RequestBlockingAction) { + ctx.clearResponseHeadersForBlocking(); + } + return flow; } private void onResponseHeader(RequestContext ctx_, String name, String value) { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy index d093190f228..9a743c21bdd 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy @@ -81,6 +81,37 @@ class AppSecRequestContextSpecification extends DDSpecification { thrown(IllegalStateException) } + void 'clearResponseHeadersForBlocking clears response headers and resets finished flag'() { + given: + ctx.addResponseHeader('content-type', 'text/html') + ctx.finishResponseHeaders() + + expect: + !ctx.responseHeaders.isEmpty() + ctx.isFinishedResponseHeaders() + + when: + ctx.clearResponseHeadersForBlocking() + + then: + ctx.responseHeaders.isEmpty() + !ctx.isFinishedResponseHeaders() + } + + void 'after clearResponseHeadersForBlocking new response headers can be added'() { + given: + ctx.addResponseHeader('content-type', 'text/html') + ctx.finishResponseHeaders() + ctx.clearResponseHeadersForBlocking() + + when: + ctx.addResponseHeader('content-type', 'application/json') + + then: + ctx.responseHeaders == ['content-type': ['application/json']] + notThrown(IllegalStateException) + } + void 'setting uri a second time is ignored, first value wins'() { when: ctx.rawURI = '/a' diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy index accab2a3365..e0356dad036 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy @@ -18,6 +18,7 @@ import datadog.trace.api.appsec.MediaType import datadog.trace.api.config.GeneralConfig import datadog.trace.api.function.TriConsumer import datadog.trace.api.function.TriFunction +import datadog.appsec.api.blocking.BlockingContentType import datadog.trace.api.gateway.BlockResponseFunction import datadog.trace.api.gateway.Flow import datadog.trace.api.gateway.IGSpanInfo @@ -225,6 +226,49 @@ class GatewayBridgeSpecification extends DDSpecification { 1 * traceSegment.setTagTop('actor.ip', '8.8.8.8') } + void 'request_end writes response headers even when no appsec events'() { + AppSecRequestContext mockAppSecCtx = Mock(AppSecRequestContext) + mockAppSecCtx.requestHeaders >> [:] + mockAppSecCtx.responseHeaders >> ['content-type': ['text/plain']] + RequestContext mockCtx = Stub(RequestContext) { + getData(RequestContextSlot.APPSEC) >> mockAppSecCtx + getTraceSegment() >> traceSegment + } + IGSpanInfo spanInfo = Mock(AgentSpan) + + when: + def flow = requestEndedCB.apply(mockCtx, spanInfo) + + then: + 1 * spanInfo.getTags() >> TagMap.fromMap([:]) + 1 * mockAppSecCtx.transferCollectedEvents() >> [] + 1 * mockAppSecCtx.close() + 1 * traceSegment.setTagTop("_dd.appsec.enabled", 1) + 1 * traceSegment.setTagTop("_dd.runtime_family", "jvm") + 1 * traceSegment.setTagTop('http.response.headers.content-type', 'text/plain') + 1 * wafMetricCollector.wafRequest(_, _, _, _, _, _, _) + flow.result == null + flow.action == Flow.Action.Noop.INSTANCE + } + + void 'response_header_done clears response headers for blocking when WAF blocks'() { + given: + def blockingFlow = Stub(Flow) { + getAction() >> new Flow.Action.RequestBlockingAction(403, BlockingContentType.AUTO) + } + eventDispatcher.getDataSubscribers(_) >> nonEmptyDsInfo + eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> blockingFlow + + when: + respHeaderCB.accept(ctx, 'content-type', 'text/html') + responseStartedCB.apply(ctx, 403) + respHeadersDoneCB.apply(ctx) + + then: + ctx.data.responseHeaders.isEmpty() + !ctx.data.finishedResponseHeaders + } + void 'bridge can collect headers'() { when: reqHeaderCB.accept(ctx, 'header1', 'value 1.1') diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/DatadogAsyncHandlerWrapper.java b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/DatadogAsyncHandlerWrapper.java index 91850d3621f..0c5d0f6c755 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/DatadogAsyncHandlerWrapper.java +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/DatadogAsyncHandlerWrapper.java @@ -8,7 +8,6 @@ import akka.stream.Materializer; import datadog.context.Context; import datadog.context.ContextScope; -import datadog.trace.api.gateway.Flow; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.instrumentation.akkahttp.appsec.BlockingResponseHelper; import scala.Function1; @@ -35,10 +34,9 @@ public Future apply(final HttpRequest request) { Future futureResponse; // handle blocking in the beginning of the request - Flow.Action.RequestBlockingAction rba; - if ((rba = span.getRequestBlockingAction()) != null) { + if (span.getRequestBlockingAction() != null) { request.discardEntityBytes(materializer); - HttpResponse response = BlockingResponseHelper.maybeCreateBlockingResponse(rba, request); + HttpResponse response = BlockingResponseHelper.maybeCreateBlockingResponse(span, request); span.getRequestContext().getTraceSegment().effectivelyBlocked(); DatadogWrapperHelper.finishSpan(context, response); return FastFuture$.MODULE$.successful().apply(response); diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java index 68d6a9d84c0..4b1657b32da 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java @@ -32,6 +32,11 @@ public static HttpResponse handleFinishForWaf(final AgentSpan span, final HttpRe HttpResponse altResponse = ((AkkaBlockResponseFunction) brf).maybeCreateAlternativeResponse(); if (altResponse != null) { // we already blocked during the request + DECORATE.callIGCallbackResponseAndHeaders( + span, + altResponse, + altResponse.status().intValue(), + AkkaHttpServerHeaders.responseGetter()); return altResponse; } } @@ -55,7 +60,12 @@ public static HttpResponse handleFinishForWaf(final AgentSpan span, final HttpRe } public static HttpResponse maybeCreateBlockingResponse(AgentSpan span, HttpRequest request) { - return maybeCreateBlockingResponse(span.getRequestBlockingAction(), request); + HttpResponse response = maybeCreateBlockingResponse(span.getRequestBlockingAction(), request); + if (response != null) { + DECORATE.callIGCallbackResponseAndHeaders( + span, response, response.status().intValue(), AkkaHttpServerHeaders.responseGetter()); + } + return response; } public static HttpResponse maybeCreateBlockingResponse( diff --git a/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java b/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java index b31435dd801..4b6c729b4ae 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java +++ b/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java @@ -1,11 +1,18 @@ package datadog.trace.instrumentation.netty40.server; +import static datadog.trace.bootstrap.instrumentation.api.Java8BytecodeBridge.spanFromContext; +import static datadog.trace.instrumentation.netty40.AttributeKeys.ANALYZED_RESPONSE_KEY; +import static datadog.trace.instrumentation.netty40.AttributeKeys.BLOCKED_RESPONSE_KEY; +import static datadog.trace.instrumentation.netty40.AttributeKeys.CONTEXT_ATTRIBUTE_KEY; +import static datadog.trace.instrumentation.netty40.server.NettyHttpServerDecorator.DECORATE; import static io.netty.handler.codec.http.HttpHeaders.setContentLength; import datadog.appsec.api.blocking.BlockingContentType; +import datadog.context.Context; import datadog.trace.api.gateway.Flow; import datadog.trace.api.internal.TraceSegment; import datadog.trace.bootstrap.blocking.BlockingActionHelper; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -112,6 +119,16 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) { } this.hasBlockedAlready = true; + + Context storedContext = ctx.channel().attr(CONTEXT_ATTRIBUTE_KEY).get(); + AgentSpan span = spanFromContext(storedContext); + if (span != null) { + DECORATE.callIGCallbackResponseAndHeaders( + span, response, httpCode, ResponseExtractAdapter.GETTER); + } + ctx.channel().attr(ANALYZED_RESPONSE_KEY).set(Boolean.TRUE); + ctx.channel().attr(BLOCKED_RESPONSE_KEY).set(Boolean.TRUE); + ReferenceCountUtil.release(msg); // write starts in the handler before the one associated with ctx diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java index 1c49fceae2f..de9a54cb192 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java @@ -1,9 +1,17 @@ package datadog.trace.instrumentation.netty41.server; +import static datadog.trace.bootstrap.instrumentation.api.Java8BytecodeBridge.spanFromContext; +import static datadog.trace.instrumentation.netty41.AttributeKeys.ANALYZED_RESPONSE_KEY; +import static datadog.trace.instrumentation.netty41.AttributeKeys.BLOCKED_RESPONSE_KEY; +import static datadog.trace.instrumentation.netty41.AttributeKeys.CONTEXT_ATTRIBUTE_KEY; +import static datadog.trace.instrumentation.netty41.server.NettyHttpServerDecorator.DECORATE; + import datadog.appsec.api.blocking.BlockingContentType; +import datadog.context.Context; import datadog.trace.api.gateway.Flow; import datadog.trace.api.internal.TraceSegment; import datadog.trace.bootstrap.blocking.BlockingActionHelper; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -112,6 +120,15 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) { this.hasBlockedAlready = true; + Context storedContext = ctx.channel().attr(CONTEXT_ATTRIBUTE_KEY).get(); + AgentSpan span = spanFromContext(storedContext); + if (span != null) { + DECORATE.callIGCallbackResponseAndHeaders( + span, response, httpCode, ResponseExtractAdapter.GETTER); + } + ctx.channel().attr(ANALYZED_RESPONSE_KEY).set(Boolean.TRUE); + ctx.channel().attr(BLOCKED_RESPONSE_KEY).set(Boolean.TRUE); + ReferenceCountUtil.release(msg); // write starts in the handler before the one associated with ctx From 47b551a1c31ce754ca564d30bba4f124012cccb3 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 4 Mar 2026 11:16:43 +0100 Subject: [PATCH 2/2] Fix and more tests --- .../AkkaHttpServerInstrumentationTest.groovy | 16 +++++++++ .../appsec/BlockingResponseHelper.java | 21 +++++++++++ .../server/BlockingResponseHandler.java | 35 +++++++++++++++++++ .../server/MaybeBlockResponseHandler.java | 4 +++ .../netty38/Netty38ServerTest.groovy | 17 +++++++++ .../server/BlockingResponseHandler.java | 16 +++++++++ .../src/test/groovy/Netty40ServerTest.groovy | 16 +++++++++ .../server/BlockingResponseHandler.java | 16 +++++++++ .../src/test/groovy/Netty41ServerTest.groovy | 16 +++++++++ 9 files changed, 157 insertions(+) diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy index 87752bd275c..c7ca4a429b9 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy @@ -199,6 +199,22 @@ abstract class AkkaHttpServerInstrumentationTest extends HttpServerTest getContextStore() { + return contextStore; + } + @Override public void writeRequested(ChannelHandlerContext ctx, MessageEvent msg) throws Exception { final ChannelTraceContext channelTraceContext = diff --git a/dd-java-agent/instrumentation/netty/netty-3.8/src/test/groovy/datadog/trace/instrumentation/netty38/Netty38ServerTest.groovy b/dd-java-agent/instrumentation/netty/netty-3.8/src/test/groovy/datadog/trace/instrumentation/netty38/Netty38ServerTest.groovy index bdcb78bafa9..9a99285fec8 100644 --- a/dd-java-agent/instrumentation/netty/netty-3.8/src/test/groovy/datadog/trace/instrumentation/netty38/Netty38ServerTest.groovy +++ b/dd-java-agent/instrumentation/netty/netty-3.8/src/test/groovy/datadog/trace/instrumentation/netty38/Netty38ServerTest.groovy @@ -63,6 +63,7 @@ import org.jboss.netty.logging.InternalLogLevel import org.jboss.netty.logging.InternalLoggerFactory import org.jboss.netty.logging.Slf4JLoggerFactory import org.jboss.netty.util.CharsetUtil +import org.junit.jupiter.api.Assumptions import spock.lang.Ignore abstract class Netty38ServerTest extends HttpServerTest { @@ -316,6 +317,22 @@ abstract class Netty38ServerTest extends HttpServerTest { boolean testBadUrl() { false } + + def 'blocking response sets http.response.headers.content-type span tag'() { + setup: + Assumptions.assumeTrue(testBlocking()) + + def request = request(SUCCESS, 'GET', null) + .addHeader(IG_BLOCK_HEADER, 'auto') + .build() + client.newCall(request).execute() + TEST_WRITER.waitForTraces(1) + + expect: + def rootSpan = TEST_WRITER.get(0).find { it.parentId == 0 } + rootSpan != null + rootSpan.tags['http.response.headers.content-type'] != null + } } class Netty38ServerV0Test extends Netty38ServerTest implements TestingNettyHttpNamingConventions.ServerV0 { diff --git a/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java b/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java index 4b6c729b4ae..2f69fa4b5e7 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java +++ b/dd-java-agent/instrumentation/netty/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/BlockingResponseHandler.java @@ -125,6 +125,7 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) { if (span != null) { DECORATE.callIGCallbackResponseAndHeaders( span, response, httpCode, ResponseExtractAdapter.GETTER); + writeBlockingResponseHeaderTags(span, response.headers()); } ctx.channel().attr(ANALYZED_RESPONSE_KEY).set(Boolean.TRUE); ctx.channel().attr(BLOCKED_RESPONSE_KEY).set(Boolean.TRUE); @@ -159,6 +160,21 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) { }); } + private static void writeBlockingResponseHeaderTags(AgentSpan span, HttpHeaders headers) { + String contentType = headers.get("Content-type"); + if (contentType != null) { + span.getRequestContext() + .getTraceSegment() + .setTagTop("http.response.headers.content-type", contentType); + } + String contentLength = headers.get("Content-Length"); + if (contentLength != null) { + span.getRequestContext() + .getTraceSegment() + .setTagTop("http.response.headers.content-length", contentLength); + } + } + @ChannelHandler.Sharable public static class IgnoreAllWritesHandler extends ChannelOutboundHandlerAdapter { public static final IgnoreAllWritesHandler INSTANCE = new IgnoreAllWritesHandler(); diff --git a/dd-java-agent/instrumentation/netty/netty-4.0/src/test/groovy/Netty40ServerTest.groovy b/dd-java-agent/instrumentation/netty/netty-4.0/src/test/groovy/Netty40ServerTest.groovy index abf29eb1b80..9c0e6c43ba5 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.0/src/test/groovy/Netty40ServerTest.groovy +++ b/dd-java-agent/instrumentation/netty/netty-4.0/src/test/groovy/Netty40ServerTest.groovy @@ -253,6 +253,22 @@ abstract class Netty40ServerTest extends HttpServerTest { boolean testBadUrl() { false } + + def 'blocking response sets http.response.headers.content-type span tag'() { + setup: + org.junit.jupiter.api.Assumptions.assumeTrue(testBlocking()) + + def request = request(SUCCESS, 'GET', null) + .addHeader(IG_BLOCK_HEADER, 'auto') + .build() + client.newCall(request).execute() + TEST_WRITER.waitForTraces(1) + + expect: + def rootSpan = TEST_WRITER.get(0).find { it.parentId == 0 } + rootSpan != null + rootSpan.tags['http.response.headers.content-type'] != null + } } class Netty40ServerV0Test extends Netty40ServerTest implements TestingNettyHttpNamingConventions.ServerV0 { diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java index de9a54cb192..7372e0c1f64 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/BlockingResponseHandler.java @@ -125,6 +125,7 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) { if (span != null) { DECORATE.callIGCallbackResponseAndHeaders( span, response, httpCode, ResponseExtractAdapter.GETTER); + writeBlockingResponseHeaderTags(span, response.headers()); } ctx.channel().attr(ANALYZED_RESPONSE_KEY).set(Boolean.TRUE); ctx.channel().attr(BLOCKED_RESPONSE_KEY).set(Boolean.TRUE); @@ -159,6 +160,21 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) { }); } + private static void writeBlockingResponseHeaderTags(AgentSpan span, HttpHeaders headers) { + String contentType = headers.get("Content-type"); + if (contentType != null) { + span.getRequestContext() + .getTraceSegment() + .setTagTop("http.response.headers.content-type", contentType); + } + String contentLength = headers.get("Content-Length"); + if (contentLength != null) { + span.getRequestContext() + .getTraceSegment() + .setTagTop("http.response.headers.content-length", contentLength); + } + } + @ChannelHandler.Sharable public static class IgnoreAllWritesHandler extends ChannelOutboundHandlerAdapter { public static final IgnoreAllWritesHandler INSTANCE = new IgnoreAllWritesHandler(); diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy index c9b37933033..a4ef1b6cbe3 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/groovy/Netty41ServerTest.groovy @@ -285,6 +285,22 @@ abstract class Netty41ServerTest extends HttpServerTest { boolean testBadUrl() { false } + + def 'blocking response sets http.response.headers.content-type span tag'() { + setup: + org.junit.jupiter.api.Assumptions.assumeTrue(testBlocking()) + + def request = request(SUCCESS, 'GET', null) + .addHeader(IG_BLOCK_HEADER, 'auto') + .build() + client.newCall(request).execute() + TEST_WRITER.waitForTraces(1) + + expect: + def rootSpan = TEST_WRITER.get(0).find { it.parentId == 0 } + rootSpan != null + rootSpan.tags['http.response.headers.content-type'] != null + } } class Netty41ServerV0Test extends Netty41ServerTest implements TestingNettyHttpNamingConventions.ServerV0 {