From 97f99331ec1179cfeafc19c3ba2263b325bb9908 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 7 May 2026 15:58:35 -0400 Subject: [PATCH 1/9] fix(vertx-web): finish vertx.route-handler via RoutingContext.addEndHandler fallback Vert.x's `Http1xServerResponse.end(Buffer, PromiseInternal)` invokes the registered `endHandler` only when `closed == false` at the moment the response body has been written. In synthetic transports such as quarkus-amazon-lambda-rest's `VirtualClientConnection` (in-memory Netty channel) the writes and the connection close happen synchronously inside `responseComplete()`, so by the time the `!closed` guard runs `closed` is already `true` and `endHandler` is silently skipped. Symptom: `RouteHandlerWrapper` starts a `vertx.route-handler` span for every route in the chain (e.g. Quarkus's AuthenticationHandler) but `EndHandlerWrapper.handle` is never called, so the span is never finished. The span dies in PendingTrace and is not enqueued on the writer. All children parented to that span (`jakarta-rs.request`, `netty.client.request`, downstream `aws.http`/`aws.apigateway` inferred spans) end up orphaned in the trace UI. Fix: also register a finish via `RoutingContext.addEndHandler`, which fires on routing-context completion regardless of underlying connection state and on both success and failure. Both paths funnel through a shared idempotent `finishHandlerSpan` so the second one to fire on real-network transports is a no-op. Verified end-to-end against a Quarkus 3.15.4 / Java 21 Lambda chain (caller -> netty.client.request -> callee) on Datadog Lambda Extension v96. Pre-fix: 5/5 invocations Started, 0/5 Finished. Post-fix: 5/5 Started, 5/5 Finished, single connected trace tree in the UI. Refs: SLES-2837 --- .../vertx_4_0/server/EndHandlerWrapper.java | 10 +------- .../vertx_4_0/server/RouteHandlerWrapper.java | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java index a71584b11a7..fd4a042edf8 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java @@ -1,9 +1,5 @@ package datadog.trace.instrumentation.vertx_4_0.server; -import static datadog.trace.instrumentation.vertx_4_0.server.RouteHandlerWrapper.HANDLER_SPAN_CONTEXT_KEY; -import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.DECORATE; - -import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -18,16 +14,12 @@ public class EndHandlerWrapper implements Handler { @Override public void handle(final Void event) { - AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); try { if (actual != null) { actual.handle(event); } } finally { - if (span != null) { - DECORATE.onResponse(span, routingContext.response()); - span.finish(); - } + RouteHandlerWrapper.finishHandlerSpan(routingContext); } } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java index 8706f816e1c..19b344b75f6 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java @@ -44,6 +44,16 @@ public void handle(final RoutingContext routingContext) { routingContext.put(HANDLER_SPAN_CONTEXT_KEY, span); routingContext.response().endHandler(new EndHandlerWrapper(routingContext)); + // Fallback finish path: HttpServerResponse.endHandler is silently skipped + // by Vert.x's Http1xServerResponse.end() when the underlying connection + // has already closed (Http1xServerResponse#end gates `endHandler.handle()` + // behind `!closed`). This happens in synthetic transports such as + // quarkus-amazon-lambda-rest's virtual Netty channel, where writes and + // close are synchronous in-memory, leaving the route-handler span unfinished + // and orphaning all jakarta-rs.request / aws.http child spans in the trace. + // RoutingContext#addEndHandler fires on routing-context completion regardless + // of underlying connection state and on both success and failure. + routingContext.addEndHandler(ar -> finishHandlerSpan(routingContext)); DECORATE.afterStart(span); span.setResourceName(DECORATE.className(actual.getClass())); } @@ -60,6 +70,19 @@ public void handle(final RoutingContext routingContext) { } } + // Idempotently finish the route-handler span. Both EndHandlerWrapper (the + // response.endHandler path) and the routingContext.addEndHandler fallback may call + // this; the first one to win clears HANDLER_SPAN_CONTEXT_KEY so the second is a no-op. + static void finishHandlerSpan(final RoutingContext routingContext) { + final AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); + if (span == null) { + return; + } + routingContext.put(HANDLER_SPAN_CONTEXT_KEY, null); + DECORATE.onResponse(span, routingContext.response()); + span.finish(); + } + private void setRoute(RoutingContext routingContext) { final AgentSpan parentSpan = routingContext.get(PARENT_SPAN_CONTEXT_KEY); if (parentSpan == null) { From 01aeb85ff9451b7e13cc0cb5cc632f5cff45ceaa Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Thu, 14 May 2026 13:05:31 -0400 Subject: [PATCH 2/9] Apply changes to vertex-web 3.4 --- .../vertx_3_4/server/EndHandlerWrapper.java | 10 +------- .../vertx_3_4/server/RouteHandlerWrapper.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java index a8cb1ebb079..f4b30bc4d44 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java @@ -1,9 +1,5 @@ package datadog.trace.instrumentation.vertx_3_4.server; -import static datadog.trace.instrumentation.vertx_3_4.server.RouteHandlerWrapper.HANDLER_SPAN_CONTEXT_KEY; -import static datadog.trace.instrumentation.vertx_3_4.server.VertxDecorator.DECORATE; - -import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -18,16 +14,12 @@ public class EndHandlerWrapper implements Handler { @Override public void handle(final Void event) { - AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); try { if (actual != null) { actual.handle(event); } } finally { - if (span != null) { - DECORATE.onResponse(span, routingContext.response()); - span.finish(); - } + RouteHandlerWrapper.finishHandlerSpan(routingContext); } } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java index 37120c2f0cd..5357a2a7bcf 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java @@ -48,6 +48,16 @@ public void handle(final RoutingContext routingContext) { routingContext.put(HANDLER_SPAN_CONTEXT_KEY, span); routingContext.response().endHandler(new EndHandlerWrapper(routingContext)); + // Fallback finish path. The response.endHandler we register above can be + // silently skipped on Vert.x 3.x in two situations: + // 1. sendFile() — only bodyEndHandler is invoked on this path. + // 2. Synthetic transports (e.g. an in-memory Netty channel) on 3.9, + // where HttpServerResponseImpl.end gates endHandler behind `!closed` + // and the response is closed synchronously by responseComplete(). + // RoutingContext.addBodyEndHandler is wired to response.bodyEndHandler, + // which HttpServerResponseImpl invokes on every response-end path across + // the 3.x range. RoutingContext.addEndHandler does not exist until 4.0. + routingContext.addBodyEndHandler(v -> finishHandlerSpan(routingContext)); DECORATE.afterStart(span); span.setResourceName(DECORATE.className(actual.getClass())); } @@ -63,6 +73,20 @@ public void handle(final RoutingContext routingContext) { } } + // Idempotently finish the route-handler span. Both EndHandlerWrapper (the + // response.endHandler path) and the routingContext.addBodyEndHandler fallback + // may call this; the first one to win clears HANDLER_SPAN_CONTEXT_KEY so the + // second is a no-op. + static void finishHandlerSpan(final RoutingContext routingContext) { + final AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); + if (span == null) { + return; + } + routingContext.put(HANDLER_SPAN_CONTEXT_KEY, null); + DECORATE.onResponse(span, routingContext.response()); + span.finish(); + } + private void setRoute(RoutingContext routingContext) { final AgentSpan parentSpan = routingContext.get(PARENT_SPAN_CONTEXT_KEY); if (parentSpan == null) { From b6dc5d18acb86e5a2eaeb25342417ac5f212a302 Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Thu, 14 May 2026 14:14:21 -0400 Subject: [PATCH 3/9] Add unit test for 3.x --- .../java/server/RouteHandlerSendFileTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java new file mode 100644 index 00000000000..3c6100e9ba4 --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java @@ -0,0 +1,116 @@ +package server; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Regression test for the vertx-web 3.x route-handler span lifecycle on the {@code + * response.sendFile(...)} path. + * + *

{@code HttpServerResponseImpl.doSendFile} (vertx-core 3.x) only invokes {@code bodyEndHandler} + * after the file is written; it never invokes {@code endHandler}. With only the {@code endHandler} + * registration (pre-fix), the {@code vertx.route-handler} span never finishes on this path, the + * trace fails to flush, and {@code waitForTraces} times out. With the fallback {@code + * addBodyEndHandler} registration, the span finishes on every response-end path. + */ +class RouteHandlerSendFileTest extends AbstractInstrumentationTest { + + private static Vertx vertx; + private static HttpServer server; + private static int port; + private static Path payload; + + @BeforeAll + static void startServer() throws Exception { + payload = Files.createTempFile("vertx-sendfile-", ".txt"); + Files.write(payload, "vertx sendFile payload\n".getBytes(StandardCharsets.UTF_8)); + payload.toFile().deleteOnExit(); + + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } + + vertx = Vertx.vertx(); + Router router = Router.router(vertx); + router + .route("/sendfile") + .handler(ctx -> ctx.response().sendFile(payload.toAbsolutePath().toString())); + + CountDownLatch ready = new CountDownLatch(1); + server = + vertx + .createHttpServer() + .requestHandler(router::accept) + .listen( + port, + result -> { + if (result.failed()) { + throw new RuntimeException("Failed to start Vert.x server", result.cause()); + } + ready.countDown(); + }); + if (!ready.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("Vert.x server did not start in time"); + } + } + + @AfterAll + static void stopServer() throws Exception { + if (server != null) { + CountDownLatch closed = new CountDownLatch(1); + server.close(ar -> closed.countDown()); + closed.await(10, TimeUnit.SECONDS); + } + if (vertx != null) { + CountDownLatch closed = new CountDownLatch(1); + vertx.close(ar -> closed.countDown()); + closed.await(10, TimeUnit.SECONDS); + } + if (payload != null) { + Files.deleteIfExists(payload); + } + } + + @Test + void sendFileFinishesRouteHandlerSpan() throws Exception { + HttpURLConnection conn = + (HttpURLConnection) new URL("http://localhost:" + port + "/sendfile").openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + assertEquals(200, conn.getResponseCode()); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + assertEquals("vertx sendFile payload", reader.readLine()); + } + + // If RouteHandlerWrapper's bodyEndHandler fallback isn't registered, the route-handler + // span never finishes on the sendFile path and this assertTraces times out. + blockUntilTracesMatch( + traces -> + traces.stream() + .flatMap(Collection::stream) + .anyMatch( + s -> + "vertx.route-handler".contentEquals(s.getOperationName()) + && s.getDurationNano() > 0)); + } +} From 490cd63eb7b2a24841452f0c78423ef4d1cb0e86 Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Thu, 14 May 2026 15:35:49 -0400 Subject: [PATCH 4/9] Clean up unit test --- .../java/server/RouteHandlerSendFileTest.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java index 3c6100e9ba4..50303793fa2 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java @@ -14,7 +14,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; @@ -102,15 +101,9 @@ void sendFileFinishesRouteHandlerSpan() throws Exception { assertEquals("vertx sendFile payload", reader.readLine()); } - // If RouteHandlerWrapper's bodyEndHandler fallback isn't registered, the route-handler - // span never finishes on the sendFile path and this assertTraces times out. - blockUntilTracesMatch( - traces -> - traces.stream() - .flatMap(Collection::stream) - .anyMatch( - s -> - "vertx.route-handler".contentEquals(s.getOperationName()) - && s.getDurationNano() > 0)); + // Strict-mode trace writes only publish a trace when every span in it has finished. + // Pre-fix: the route-handler span never finishes on the sendFile path, so the trace + // is never published and this call throws TimeoutException. + writer.waitForTraces(1); } } From 07787a92b302f31ec4fb27c06a1e934be0ea2e15 Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Thu, 14 May 2026 16:35:41 -0400 Subject: [PATCH 5/9] Add junit jupiter to gradle setup --- .../vertx/vertx-web/vertx-web-3.4/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle index 56311523214..6dd15db800f 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle @@ -46,6 +46,12 @@ dependencies { testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') + // Make JUnit Jupiter API explicit for the Java JUnit 5 regression test + // (RouteHandlerSendFileTest). The shared gradle/java_deps.gradle only declares + // JUnit 5 as testRuntimeOnly; otherwise we'd be relying on Spock 2.x's transitive + // junit-jupiter-api on testCompileClasspath, which is implicit and brittle. + testImplementation libs.junit.jupiter + latestDepTestImplementation group: 'io.vertx', name: 'vertx-web', version: '3.+' latestDepTestImplementation group: 'io.vertx', name: 'vertx-web-client', version: '3.+' } From a3b26df8a9ce9f58589d05175491f197d29c51d5 Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Mon, 18 May 2026 10:35:47 -0400 Subject: [PATCH 6/9] wrap exception handling --- .../server/ExceptionHandlerWrapper.java | 25 +++++++ .../server/ExceptionHandlerWrapperAdvice.java | 29 ++++++++ ...rverResponseEndHandlerInstrumentation.java | 1 + ...sponseExceptionHandlerInstrumentation.java | 44 +++++++++++ .../server/RouteHandlerInstrumentation.java | 1 + .../vertx_3_4/server/RouteHandlerWrapper.java | 30 ++++---- .../RouteHandlerExceptionHandlerTest.java | 74 +++++++++++++++++++ 7 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java new file mode 100644 index 00000000000..951946badd6 --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java @@ -0,0 +1,25 @@ +package datadog.trace.instrumentation.vertx_3_4.server; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ExceptionHandlerWrapper implements Handler { + private final RoutingContext routingContext; + + public Handler actual; + + ExceptionHandlerWrapper(RoutingContext routingContext) { + this.routingContext = routingContext; + } + + @Override + public void handle(final Throwable event) { + try { + if (actual != null) { + actual.handle(event); + } + } finally { + RouteHandlerWrapper.finishHandlerSpan(routingContext); + } + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java new file mode 100644 index 00000000000..9bbcfa4ecef --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java @@ -0,0 +1,29 @@ +package datadog.trace.instrumentation.vertx_3_4.server; + +import io.vertx.core.Handler; +import net.bytebuddy.asm.Advice; + +public class ExceptionHandlerWrapperAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.FieldValue(value = "exceptionHandler", readOnly = false) + final Handler exceptionHandler, + @Advice.Argument(value = 0, readOnly = false) Handler handler) { + // In case the handler instrumentation executes twice on the same response + if (exceptionHandler instanceof ExceptionHandlerWrapper + && handler instanceof ExceptionHandlerWrapper) { + return; + } + // If an exception handler was already registered when our wrapper is registered, we save the + // one that existed before + if (handler instanceof ExceptionHandlerWrapper && exceptionHandler != null) { + ((ExceptionHandlerWrapper) handler).actual = exceptionHandler; + + // If the user registers an exception handler and ours has already been registered then we + // wrap the user's handler and swap the function argument for the wrapper + } else if (exceptionHandler instanceof ExceptionHandlerWrapper) { + ((ExceptionHandlerWrapper) exceptionHandler).actual = handler; + handler = exceptionHandler; + } + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java index 28aa02442ff..e3367df2d9f 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java @@ -20,6 +20,7 @@ public HttpServerResponseEndHandlerInstrumentation() { public String[] helperClassNames() { return new String[] { packageName + ".EndHandlerWrapper", + packageName + ".ExceptionHandlerWrapper", packageName + ".RouteHandlerWrapper", packageName + ".VertxDecorator", packageName + ".VertxDecorator$VertxURIDataAdapter", diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java new file mode 100644 index 00000000000..687378eae33 --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java @@ -0,0 +1,44 @@ +package datadog.trace.instrumentation.vertx_3_4.server; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; + +@AutoService(InstrumenterModule.class) +public class HttpServerResponseExceptionHandlerInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + public HttpServerResponseExceptionHandlerInstrumentation() { + super("vertx", "vertx-3.4"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".EndHandlerWrapper", + packageName + ".ExceptionHandlerWrapper", + packageName + ".RouteHandlerWrapper", + packageName + ".VertxDecorator", + packageName + ".VertxDecorator$VertxURIDataAdapter", + }; + } + + @Override + public String instrumentedType() { + return "io.vertx.core.http.impl.HttpServerResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("exceptionHandler")) + .and(isPublic()) + .and(takesArgument(0, named("io.vertx.core.Handler"))), + packageName + ".ExceptionHandlerWrapperAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java index 6e4c5445d7d..62d99d89b48 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java @@ -21,6 +21,7 @@ public RouteHandlerInstrumentation() { public String[] helperClassNames() { return new String[] { packageName + ".EndHandlerWrapper", + packageName + ".ExceptionHandlerWrapper", packageName + ".RouteHandlerWrapper", packageName + ".VertxDecorator", packageName + ".VertxDecorator$VertxURIDataAdapter", diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java index 5357a2a7bcf..aa841d4465a 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java @@ -47,17 +47,21 @@ public void handle(final RoutingContext routingContext) { span = startSpan("vertx", INSTRUMENTATION_NAME); routingContext.put(HANDLER_SPAN_CONTEXT_KEY, span); + // Vert.x 3.x has no single hook that fires on every response outcome + // (RoutingContext.addEndHandler is 4.0+). We register three: + // - response.endHandler: normal response-end path. + // - response.bodyEndHandler (via RoutingContext.addBodyEndHandler): + // covers sendFile() success and synthetic-transport cases where + // HttpServerResponseImpl.end gates endHandler behind `!closed` + // and the response is closed synchronously by responseComplete() + // (e.g. quarkus-amazon-lambda-rest's in-memory Netty channel). + // - response.exceptionHandler: covers I/O failures surfaced via + // HttpServerResponseImpl.handleException (non-CLOSED_EXCEPTION), + // where neither endHandler nor bodyEndHandler fires. + // finishHandlerSpan is idempotent; whichever hook fires first wins. routingContext.response().endHandler(new EndHandlerWrapper(routingContext)); - // Fallback finish path. The response.endHandler we register above can be - // silently skipped on Vert.x 3.x in two situations: - // 1. sendFile() — only bodyEndHandler is invoked on this path. - // 2. Synthetic transports (e.g. an in-memory Netty channel) on 3.9, - // where HttpServerResponseImpl.end gates endHandler behind `!closed` - // and the response is closed synchronously by responseComplete(). - // RoutingContext.addBodyEndHandler is wired to response.bodyEndHandler, - // which HttpServerResponseImpl invokes on every response-end path across - // the 3.x range. RoutingContext.addEndHandler does not exist until 4.0. routingContext.addBodyEndHandler(v -> finishHandlerSpan(routingContext)); + routingContext.response().exceptionHandler(new ExceptionHandlerWrapper(routingContext)); DECORATE.afterStart(span); span.setResourceName(DECORATE.className(actual.getClass())); } @@ -73,10 +77,10 @@ public void handle(final RoutingContext routingContext) { } } - // Idempotently finish the route-handler span. Both EndHandlerWrapper (the - // response.endHandler path) and the routingContext.addBodyEndHandler fallback - // may call this; the first one to win clears HANDLER_SPAN_CONTEXT_KEY so the - // second is a no-op. + // Idempotently finish the route-handler span. Any of the three registered + // hooks (EndHandlerWrapper, the addBodyEndHandler fallback, or + // ExceptionHandlerWrapper) may call this; the first one to win clears + // HANDLER_SPAN_CONTEXT_KEY so the others are no-ops. static void finishHandlerSpan(final RoutingContext routingContext) { final AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); if (span == null) { diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java new file mode 100644 index 00000000000..adabc46c200 --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java @@ -0,0 +1,74 @@ +package server; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.instrumentation.vertx_3_4.server.RouteHandlerWrapper; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.RoutingContext; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +/** + * Unit test for the vertx-web 3.x route-handler span lifecycle on the {@code + * response.exceptionHandler} path. + * + *

{@code HttpServerResponseImpl.handleException} is invoked by Vert.x on non-{@code + * CLOSED_EXCEPTION} I/O failures of the response. Neither {@code endHandler} nor {@code + * bodyEndHandler} fires on this path, so the route-handler span would leak without an exception + * handler registered. This test drives the registered handler directly and asserts the trace is + * flushed. + */ +class RouteHandlerExceptionHandlerTest extends AbstractInstrumentationTest { + + @Test + void exceptionHandlerFinishesRouteHandlerSpan() throws Exception { + final Map store = new HashMap<>(); + + RoutingContext routingContext = mock(RoutingContext.class); + HttpServerResponse response = mock(HttpServerResponse.class); + HttpServerRequest request = mock(HttpServerRequest.class); + Route route = mock(Route.class); + + when(routingContext.response()).thenReturn(response); + when(routingContext.request()).thenReturn(request); + when(routingContext.currentRoute()).thenReturn(route); + when(routingContext.get(anyString())).thenAnswer(inv -> store.get(inv.getArgument(0))); + doAnswer( + inv -> { + store.put(inv.getArgument(0), inv.getArgument(1)); + return null; + }) + .when(routingContext) + .put(anyString(), any()); + when(request.rawMethod()).thenReturn("GET"); + when(route.getPath()).thenReturn("/exception-path"); + + Handler userHandler = ctx -> {}; + RouteHandlerWrapper wrapper = new RouteHandlerWrapper(userHandler); + + wrapper.handle(routingContext); + + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(Handler.class); + verify(response).exceptionHandler(captor.capture()); + + captor.getValue().handle(new IOException("simulated response I/O failure")); + + // Strict-mode trace writes only publish a trace when every span in it has finished. + // If the registered exception handler did not finish the route-handler span, + // waitForTraces would throw TimeoutException. + writer.waitForTraces(1); + } +} From a43cc5ba2e4e248c5f26ef537cdd7165b425ef39 Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Mon, 18 May 2026 15:56:13 -0400 Subject: [PATCH 7/9] Adding tests, cleaning up exception handling --- .../vertx-web/vertx-web-3.4/build.gradle | 11 +- .../server/ExceptionHandlerWrapper.java | 25 --- .../server/ExceptionHandlerWrapperAdvice.java | 29 ---- ...rverResponseEndHandlerInstrumentation.java | 1 - ...sponseExceptionHandlerInstrumentation.java | 44 ----- .../server/RouteHandlerInstrumentation.java | 1 - .../vertx_3_4/server/RouteHandlerWrapper.java | 18 +- .../impl/ResponseExceptionFiringHelper.java | 17 ++ .../RouteHandlerExceptionHandlerTest.java | 157 ++++++++++++------ .../java/server/RouteHandlerSendFileTest.java | 22 ++- .../vertx-web/vertx-web-4.0/build.gradle | 7 + .../impl/ResponseExceptionFiringHelper.java | 17 ++ .../RouteHandlerExceptionHandlerTest.java | 133 +++++++++++++++ 13 files changed, 318 insertions(+), 164 deletions(-) delete mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java delete mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java delete mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java create mode 100644 dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle index 6dd15db800f..66cad33a760 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle @@ -42,14 +42,13 @@ dependencies { testImplementation group: 'io.vertx', name: 'vertx-web', version: '3.4.0' testImplementation group: 'io.vertx', name: 'vertx-web-client', version: '3.4.0' - testImplementation group: 'org.mockito', name: 'mockito-inline', version: '4.11.0' - testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') - // Make JUnit Jupiter API explicit for the Java JUnit 5 regression test - // (RouteHandlerSendFileTest). The shared gradle/java_deps.gradle only declares - // JUnit 5 as testRuntimeOnly; otherwise we'd be relying on Spock 2.x's transitive - // junit-jupiter-api on testCompileClasspath, which is implicit and brittle. + // Make JUnit Jupiter API explicit for the Java JUnit 5 tests in this module + // (RouteHandlerSendFileTest, RouteHandlerExceptionHandlerTest). The shared + // gradle/java_deps.gradle only declares JUnit 5 as testRuntimeOnly; otherwise + // we'd be relying on Spock 2.x's transitive junit-jupiter-api on + // testCompileClasspath, which is implicit and brittle. testImplementation libs.junit.jupiter latestDepTestImplementation group: 'io.vertx', name: 'vertx-web', version: '3.+' diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java deleted file mode 100644 index 951946badd6..00000000000 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapper.java +++ /dev/null @@ -1,25 +0,0 @@ -package datadog.trace.instrumentation.vertx_3_4.server; - -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; - -public class ExceptionHandlerWrapper implements Handler { - private final RoutingContext routingContext; - - public Handler actual; - - ExceptionHandlerWrapper(RoutingContext routingContext) { - this.routingContext = routingContext; - } - - @Override - public void handle(final Throwable event) { - try { - if (actual != null) { - actual.handle(event); - } - } finally { - RouteHandlerWrapper.finishHandlerSpan(routingContext); - } - } -} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java deleted file mode 100644 index 9bbcfa4ecef..00000000000 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/ExceptionHandlerWrapperAdvice.java +++ /dev/null @@ -1,29 +0,0 @@ -package datadog.trace.instrumentation.vertx_3_4.server; - -import io.vertx.core.Handler; -import net.bytebuddy.asm.Advice; - -public class ExceptionHandlerWrapperAdvice { - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void wrapHandler( - @Advice.FieldValue(value = "exceptionHandler", readOnly = false) - final Handler exceptionHandler, - @Advice.Argument(value = 0, readOnly = false) Handler handler) { - // In case the handler instrumentation executes twice on the same response - if (exceptionHandler instanceof ExceptionHandlerWrapper - && handler instanceof ExceptionHandlerWrapper) { - return; - } - // If an exception handler was already registered when our wrapper is registered, we save the - // one that existed before - if (handler instanceof ExceptionHandlerWrapper && exceptionHandler != null) { - ((ExceptionHandlerWrapper) handler).actual = exceptionHandler; - - // If the user registers an exception handler and ours has already been registered then we - // wrap the user's handler and swap the function argument for the wrapper - } else if (exceptionHandler instanceof ExceptionHandlerWrapper) { - ((ExceptionHandlerWrapper) exceptionHandler).actual = handler; - handler = exceptionHandler; - } - } -} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java index e3367df2d9f..28aa02442ff 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseEndHandlerInstrumentation.java @@ -20,7 +20,6 @@ public HttpServerResponseEndHandlerInstrumentation() { public String[] helperClassNames() { return new String[] { packageName + ".EndHandlerWrapper", - packageName + ".ExceptionHandlerWrapper", packageName + ".RouteHandlerWrapper", packageName + ".VertxDecorator", packageName + ".VertxDecorator$VertxURIDataAdapter", diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java deleted file mode 100644 index 687378eae33..00000000000 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/HttpServerResponseExceptionHandlerInstrumentation.java +++ /dev/null @@ -1,44 +0,0 @@ -package datadog.trace.instrumentation.vertx_3_4.server; - -import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; -import static net.bytebuddy.matcher.ElementMatchers.isPublic; -import static net.bytebuddy.matcher.ElementMatchers.takesArgument; - -import com.google.auto.service.AutoService; -import datadog.trace.agent.tooling.Instrumenter; -import datadog.trace.agent.tooling.InstrumenterModule; - -@AutoService(InstrumenterModule.class) -public class HttpServerResponseExceptionHandlerInstrumentation extends InstrumenterModule.Tracing - implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { - public HttpServerResponseExceptionHandlerInstrumentation() { - super("vertx", "vertx-3.4"); - } - - @Override - public String[] helperClassNames() { - return new String[] { - packageName + ".EndHandlerWrapper", - packageName + ".ExceptionHandlerWrapper", - packageName + ".RouteHandlerWrapper", - packageName + ".VertxDecorator", - packageName + ".VertxDecorator$VertxURIDataAdapter", - }; - } - - @Override - public String instrumentedType() { - return "io.vertx.core.http.impl.HttpServerResponseImpl"; - } - - @Override - public void methodAdvice(MethodTransformer transformer) { - transformer.applyAdvice( - isMethod() - .and(named("exceptionHandler")) - .and(isPublic()) - .and(takesArgument(0, named("io.vertx.core.Handler"))), - packageName + ".ExceptionHandlerWrapperAdvice"); - } -} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java index 62d99d89b48..6e4c5445d7d 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java @@ -21,7 +21,6 @@ public RouteHandlerInstrumentation() { public String[] helperClassNames() { return new String[] { packageName + ".EndHandlerWrapper", - packageName + ".ExceptionHandlerWrapper", packageName + ".RouteHandlerWrapper", packageName + ".VertxDecorator", packageName + ".VertxDecorator$VertxURIDataAdapter", diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java index aa841d4465a..fa31909c7e4 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java @@ -57,11 +57,19 @@ public void handle(final RoutingContext routingContext) { // (e.g. quarkus-amazon-lambda-rest's in-memory Netty channel). // - response.exceptionHandler: covers I/O failures surfaced via // HttpServerResponseImpl.handleException (non-CLOSED_EXCEPTION), - // where neither endHandler nor bodyEndHandler fires. + // where neither endHandler nor bodyEndHandler fires. Note that + // response.exceptionHandler is a single-slot setter; if the user + // installs their own exception handler later in the request, ours + // is overwritten and the span will leak on this path again. // finishHandlerSpan is idempotent; whichever hook fires first wins. + // + // Known remaining gap: sendFile() failures on file-not-found or + // IOException-during-open log via HttpServerResponseImpl and return + // without firing any of the three hooks above (when the caller did + // not pass a resultHandler), so the span still leaks on that path. routingContext.response().endHandler(new EndHandlerWrapper(routingContext)); routingContext.addBodyEndHandler(v -> finishHandlerSpan(routingContext)); - routingContext.response().exceptionHandler(new ExceptionHandlerWrapper(routingContext)); + routingContext.response().exceptionHandler(t -> finishHandlerSpan(routingContext)); DECORATE.afterStart(span); span.setResourceName(DECORATE.className(actual.getClass())); } @@ -78,9 +86,9 @@ public void handle(final RoutingContext routingContext) { } // Idempotently finish the route-handler span. Any of the three registered - // hooks (EndHandlerWrapper, the addBodyEndHandler fallback, or - // ExceptionHandlerWrapper) may call this; the first one to win clears - // HANDLER_SPAN_CONTEXT_KEY so the others are no-ops. + // hooks (EndHandlerWrapper, the addBodyEndHandler fallback, or the + // response.exceptionHandler lambda) may call this; the first one to win + // clears HANDLER_SPAN_CONTEXT_KEY so the others are no-ops. static void finishHandlerSpan(final RoutingContext routingContext) { final AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); if (span == null) { diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java new file mode 100644 index 00000000000..eca10e8c817 --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java @@ -0,0 +1,17 @@ +package io.vertx.core.http.impl; + +import io.vertx.core.http.HttpServerResponse; + +/** + * Test-side bridge that fires the package-private {@code HttpServerResponseImpl.handleException} on + * a Vert.x 3.x server response. Used by {@code server.RouteHandlerExceptionHandlerTest} to + * deterministically reproduce the non-{@code CLOSED_EXCEPTION} I/O-failure path that Vert.x exposes + * via {@code response.exceptionHandler(...)}. + */ +public final class ResponseExceptionFiringHelper { + private ResponseExceptionFiringHelper() {} + + public static void fireException(HttpServerResponse response, Throwable cause) { + ((HttpServerResponseImpl) response).handleException(cause); + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java index adabc46c200..5ca21392ed4 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java @@ -1,74 +1,131 @@ package server; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; import datadog.trace.agent.test.AbstractInstrumentationTest; -import datadog.trace.instrumentation.vertx_3_4.server.RouteHandlerWrapper; -import io.vertx.core.Handler; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.RoutingContext; +import datadog.trace.api.DDSpanTypes; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.impl.ResponseExceptionFiringHelper; +import io.vertx.ext.web.Router; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; /** - * Unit test for the vertx-web 3.x route-handler span lifecycle on the {@code + * Regression test for the vertx-web 3.x route-handler span lifecycle on the {@code * response.exceptionHandler} path. * *

{@code HttpServerResponseImpl.handleException} is invoked by Vert.x on non-{@code * CLOSED_EXCEPTION} I/O failures of the response. Neither {@code endHandler} nor {@code * bodyEndHandler} fires on this path, so the route-handler span would leak without an exception - * handler registered. This test drives the registered handler directly and asserts the trace is - * flushed. + * handler registered. The route handler here fires {@code handleException} directly via {@link + * ResponseExceptionFiringHelper} (the package-private method Vert.x itself uses internally), then + * calls {@code response.end()} normally so the HTTP client gets a response. */ class RouteHandlerExceptionHandlerTest extends AbstractInstrumentationTest { - @Test - void exceptionHandlerFinishesRouteHandlerSpan() throws Exception { - final Map store = new HashMap<>(); + private static Vertx vertx; + private static HttpServer server; + private static int port; - RoutingContext routingContext = mock(RoutingContext.class); - HttpServerResponse response = mock(HttpServerResponse.class); - HttpServerRequest request = mock(HttpServerRequest.class); - Route route = mock(Route.class); + @BeforeAll + static void startServer() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } - when(routingContext.response()).thenReturn(response); - when(routingContext.request()).thenReturn(request); - when(routingContext.currentRoute()).thenReturn(route); - when(routingContext.get(anyString())).thenAnswer(inv -> store.get(inv.getArgument(0))); - doAnswer( - inv -> { - store.put(inv.getArgument(0), inv.getArgument(1)); - return null; - }) - .when(routingContext) - .put(anyString(), any()); - when(request.rawMethod()).thenReturn("GET"); - when(route.getPath()).thenReturn("/exception-path"); + vertx = Vertx.vertx(); + Router router = Router.router(vertx); + router + .route("/fail") + .handler( + ctx -> { + ResponseExceptionFiringHelper.fireException( + ctx.response(), new IOException("simulated response I/O failure")); + try { + ctx.response().setStatusCode(500).end("error"); + } catch (IllegalStateException ignore) { + // handleException may have left the response in a state where end() is rejected; + // the span is already finished by our registered exception handler. + } + }); - Handler userHandler = ctx -> {}; - RouteHandlerWrapper wrapper = new RouteHandlerWrapper(userHandler); - - wrapper.handle(routingContext); + CountDownLatch ready = new CountDownLatch(1); + server = + vertx + .createHttpServer() + .requestHandler(router::accept) + .listen( + port, + result -> { + if (result.failed()) { + throw new RuntimeException("Failed to start Vert.x server", result.cause()); + } + ready.countDown(); + }); + if (!ready.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("Vert.x server did not start in time"); + } + } - @SuppressWarnings("unchecked") - ArgumentCaptor> captor = ArgumentCaptor.forClass(Handler.class); - verify(response).exceptionHandler(captor.capture()); + @AfterAll + static void stopServer() throws Exception { + if (server != null) { + CountDownLatch closed = new CountDownLatch(1); + server.close(ar -> closed.countDown()); + closed.await(10, TimeUnit.SECONDS); + } + if (vertx != null) { + CountDownLatch closed = new CountDownLatch(1); + vertx.close(ar -> closed.countDown()); + closed.await(10, TimeUnit.SECONDS); + } + } - captor.getValue().handle(new IOException("simulated response I/O failure")); + @Test + void exceptionHandlerFinishesRouteHandlerSpan() throws Exception { + HttpURLConnection conn = + (HttpURLConnection) new URL("http://localhost:" + port + "/fail").openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + try { + // We don't care about the response code or body — only that the trace flushes. + conn.getResponseCode(); + } catch (IOException ignore) { + // If end() was rejected after handleException, the client may see a closed connection. + } finally { + conn.disconnect(); + } - // Strict-mode trace writes only publish a trace when every span in it has finished. - // If the registered exception handler did not finish the route-handler span, - // waitForTraces would throw TimeoutException. - writer.waitForTraces(1); + // Strict-mode trace writes only publish when every span in the trace has finished. + // If response.exceptionHandler did not finish the route-handler span, assertTraces + // would time out waiting for the trace to flush. + // Span operation names are stored as UTF8BytesString, whose equals() rejects String + // arguments, so match via a quoted Pattern instead of the String overload. + // The netty.request span is marked as errored because the route handler ends with + // HTTP 500; the route-handler span is finished by our exception handler before + // setStatusCode(500), so it sees status=200 (default) and is not errored. + assertTraces( + trace( + SORT_BY_START_TIME, + span() + .operationName(Pattern.compile(Pattern.quote("netty.request"))) + .type(DDSpanTypes.HTTP_SERVER) + .error(), + span() + .childOfPrevious() + .operationName(Pattern.compile(Pattern.quote("vertx.route-handler"))) + .type(DDSpanTypes.HTTP_SERVER))); } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java index 50303793fa2..9de8b1b7af0 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java @@ -1,8 +1,12 @@ package server; +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; import static org.junit.jupiter.api.Assertions.assertEquals; import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.api.DDSpanTypes; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; import io.vertx.ext.web.Router; @@ -16,6 +20,7 @@ import java.nio.file.Path; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -27,7 +32,7 @@ *

{@code HttpServerResponseImpl.doSendFile} (vertx-core 3.x) only invokes {@code bodyEndHandler} * after the file is written; it never invokes {@code endHandler}. With only the {@code endHandler} * registration (pre-fix), the {@code vertx.route-handler} span never finishes on this path, the - * trace fails to flush, and {@code waitForTraces} times out. With the fallback {@code + * trace fails to flush, and {@code assertTraces} times out. With the fallback {@code * addBodyEndHandler} registration, the span finishes on every response-end path. */ class RouteHandlerSendFileTest extends AbstractInstrumentationTest { @@ -103,7 +108,18 @@ void sendFileFinishesRouteHandlerSpan() throws Exception { // Strict-mode trace writes only publish a trace when every span in it has finished. // Pre-fix: the route-handler span never finishes on the sendFile path, so the trace - // is never published and this call throws TimeoutException. - writer.waitForTraces(1); + // is never published and assertTraces times out waiting for the trace to flush. + // Span operation names are stored as UTF8BytesString, whose equals() rejects String + // arguments, so match via a quoted Pattern instead of the String overload. + assertTraces( + trace( + SORT_BY_START_TIME, + span() + .operationName(Pattern.compile(Pattern.quote("netty.request"))) + .type(DDSpanTypes.HTTP_SERVER), + span() + .childOfPrevious() + .operationName(Pattern.compile(Pattern.quote("vertx.route-handler"))) + .type(DDSpanTypes.HTTP_SERVER))); } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle index 096110203b4..08c19f83656 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle @@ -45,6 +45,13 @@ dependencies { testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') + // Make JUnit Jupiter API explicit for the Java JUnit 5 tests in this module + // (RouteHandlerExceptionHandlerTest). The shared gradle/java_deps.gradle only + // declares JUnit 5 as testRuntimeOnly; otherwise we'd be relying on Spock 2.x's + // transitive junit-jupiter-api on testCompileClasspath, which is implicit and + // brittle. + testImplementation libs.junit.jupiter + testRuntimeOnly project(':dd-java-agent:instrumentation:jackson-core:jackson-core-common') testRuntimeOnly project(':dd-java-agent:instrumentation:netty:netty-buffer-4.0') diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java new file mode 100644 index 00000000000..be8c55eff6f --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java @@ -0,0 +1,17 @@ +package io.vertx.core.http.impl; + +import io.vertx.core.http.HttpServerResponse; + +/** + * Test-side bridge that fires the package-private {@code Http1xServerResponse.handleException} on a + * Vert.x 4.x server response. Used by {@code server.RouteHandlerExceptionHandlerTest} to + * deterministically reproduce the non-{@code CLOSED_EXCEPTION} I/O-failure path that Vert.x exposes + * via {@code response.exceptionHandler(...)}. + */ +public final class ResponseExceptionFiringHelper { + private ResponseExceptionFiringHelper() {} + + public static void fireException(HttpServerResponse response, Throwable cause) { + ((Http1xServerResponse) response).handleException(cause); + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java new file mode 100644 index 00000000000..e717c6e97c4 --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java @@ -0,0 +1,133 @@ +package server; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.api.DDSpanTypes; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.impl.ResponseExceptionFiringHelper; +import io.vertx.ext.web.Router; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Regression test for the vertx-web 4.x route-handler span lifecycle on the {@code + * response.exceptionHandler} path. + * + *

{@code Http1xServerResponse.handleException} is invoked by Vert.x on non-{@code + * CLOSED_EXCEPTION} I/O failures of the response. Without {@code RoutingContext.addEndHandler(...)} + * registered, only the wrapped {@code response.endHandler} could finish the route-handler span — + * and that hook does not fire on the exception path. With the {@code addEndHandler} fallback in + * {@code RouteHandlerWrapper}, the routing context's internal exception handler fires our + * completion callback regardless of which response hook surfaces the error. The route handler here + * fires {@code handleException} directly via {@link ResponseExceptionFiringHelper}, then calls + * {@code response.end()} normally so the HTTP client gets a response. + */ +class RouteHandlerExceptionHandlerTest extends AbstractInstrumentationTest { + + private static Vertx vertx; + private static HttpServer server; + private static int port; + + @BeforeAll + static void startServer() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } + + vertx = Vertx.vertx(); + Router router = Router.router(vertx); + router + .route("/fail") + .handler( + ctx -> { + ResponseExceptionFiringHelper.fireException( + ctx.response(), new IOException("simulated response I/O failure")); + try { + ctx.response().setStatusCode(500).end("error"); + } catch (IllegalStateException ignore) { + // handleException may have left the response in a state where end() is rejected; + // the span is already finished by our addEndHandler callback. + } + }); + + CountDownLatch ready = new CountDownLatch(1); + server = + vertx + .createHttpServer() + .requestHandler(router) + .listen( + port, + result -> { + if (result.failed()) { + throw new RuntimeException("Failed to start Vert.x server", result.cause()); + } + ready.countDown(); + }); + if (!ready.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("Vert.x server did not start in time"); + } + } + + @AfterAll + static void stopServer() throws Exception { + if (server != null) { + CountDownLatch closed = new CountDownLatch(1); + server.close(ar -> closed.countDown()); + closed.await(10, TimeUnit.SECONDS); + } + if (vertx != null) { + CountDownLatch closed = new CountDownLatch(1); + vertx.close(ar -> closed.countDown()); + closed.await(10, TimeUnit.SECONDS); + } + } + + @Test + void exceptionHandlerFinishesRouteHandlerSpan() throws Exception { + HttpURLConnection conn = + (HttpURLConnection) new URL("http://localhost:" + port + "/fail").openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + try { + // We don't care about the response code or body — only that the trace flushes. + conn.getResponseCode(); + } catch (IOException ignore) { + // If end() was rejected after handleException, the client may see a closed connection. + } finally { + conn.disconnect(); + } + + // Strict-mode trace writes only publish when every span in the trace has finished. + // If addEndHandler did not finish the route-handler span, assertTraces would time out + // waiting for the trace to flush. + // Span operation names are stored as UTF8BytesString, whose equals() rejects String + // arguments, so match via a quoted Pattern instead of the String overload. + // The netty.request span is marked as errored because the route handler ends with + // HTTP 500; the route-handler span is finished by our addEndHandler callback before + // setStatusCode(500), so it sees status=200 (default) and is not errored. + assertTraces( + trace( + SORT_BY_START_TIME, + span() + .operationName(Pattern.compile(Pattern.quote("netty.request"))) + .type(DDSpanTypes.HTTP_SERVER) + .error(), + span() + .childOfPrevious() + .operationName(Pattern.compile(Pattern.quote("vertx.route-handler"))) + .type(DDSpanTypes.HTTP_SERVER))); + } +} From 95980481254db9befabb20eadf04bb3389403cfc Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Mon, 18 May 2026 16:17:35 -0400 Subject: [PATCH 8/9] cleaning comments --- .../vertx-web/vertx-web-3.4/build.gradle | 5 ----- .../vertx_3_4/server/RouteHandlerWrapper.java | 15 +------------ .../impl/ResponseExceptionFiringHelper.java | 8 +++---- .../RouteHandlerExceptionHandlerTest.java | 20 ++++++----------- .../java/server/RouteHandlerSendFileTest.java | 16 +++++--------- .../vertx-web/vertx-web-4.0/build.gradle | 5 ----- .../impl/ResponseExceptionFiringHelper.java | 8 +++---- .../RouteHandlerExceptionHandlerTest.java | 22 ++++++++----------- 8 files changed, 31 insertions(+), 68 deletions(-) diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle index 66cad33a760..2344e2a0a81 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/build.gradle @@ -44,11 +44,6 @@ dependencies { testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') - // Make JUnit Jupiter API explicit for the Java JUnit 5 tests in this module - // (RouteHandlerSendFileTest, RouteHandlerExceptionHandlerTest). The shared - // gradle/java_deps.gradle only declares JUnit 5 as testRuntimeOnly; otherwise - // we'd be relying on Spock 2.x's transitive junit-jupiter-api on - // testCompileClasspath, which is implicit and brittle. testImplementation libs.junit.jupiter latestDepTestImplementation group: 'io.vertx', name: 'vertx-web', version: '3.+' diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java index fa31909c7e4..b001826420e 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java @@ -47,20 +47,7 @@ public void handle(final RoutingContext routingContext) { span = startSpan("vertx", INSTRUMENTATION_NAME); routingContext.put(HANDLER_SPAN_CONTEXT_KEY, span); - // Vert.x 3.x has no single hook that fires on every response outcome - // (RoutingContext.addEndHandler is 4.0+). We register three: - // - response.endHandler: normal response-end path. - // - response.bodyEndHandler (via RoutingContext.addBodyEndHandler): - // covers sendFile() success and synthetic-transport cases where - // HttpServerResponseImpl.end gates endHandler behind `!closed` - // and the response is closed synchronously by responseComplete() - // (e.g. quarkus-amazon-lambda-rest's in-memory Netty channel). - // - response.exceptionHandler: covers I/O failures surfaced via - // HttpServerResponseImpl.handleException (non-CLOSED_EXCEPTION), - // where neither endHandler nor bodyEndHandler fires. Note that - // response.exceptionHandler is a single-slot setter; if the user - // installs their own exception handler later in the request, ours - // is overwritten and the span will leak on this path again. + // Register three hooks that fire on response outcome: // finishHandlerSpan is idempotent; whichever hook fires first wins. // // Known remaining gap: sendFile() failures on file-not-found or diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java index eca10e8c817..13e2fe7def0 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java @@ -3,10 +3,10 @@ import io.vertx.core.http.HttpServerResponse; /** - * Test-side bridge that fires the package-private {@code HttpServerResponseImpl.handleException} on - * a Vert.x 3.x server response. Used by {@code server.RouteHandlerExceptionHandlerTest} to - * deterministically reproduce the non-{@code CLOSED_EXCEPTION} I/O-failure path that Vert.x exposes - * via {@code response.exceptionHandler(...)}. + * Test-side bridge that fires the package-private HttpServerResponseImpl.handleException on + * a Vert.x 3.x server response. Used by server.RouteHandlerExceptionHandlerTest to + * deterministically reproduce the non-CLOSED_EXCEPTION I/O-failure path that Vert.x exposes + * via response.exceptionHandler(...). */ public final class ResponseExceptionFiringHelper { private ResponseExceptionFiringHelper() {} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java index 5ca21392ed4..897458d4e49 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java @@ -22,15 +22,14 @@ import org.junit.jupiter.api.Test; /** - * Regression test for the vertx-web 3.x route-handler span lifecycle on the {@code - * response.exceptionHandler} path. + * Regression test for the vertx-web 3.x route-handler span lifecycle on the response.exceptionHandler path. * - *

{@code HttpServerResponseImpl.handleException} is invoked by Vert.x on non-{@code - * CLOSED_EXCEPTION} I/O failures of the response. Neither {@code endHandler} nor {@code - * bodyEndHandler} fires on this path, so the route-handler span would leak without an exception - * handler registered. The route handler here fires {@code handleException} directly via {@link - * ResponseExceptionFiringHelper} (the package-private method Vert.x itself uses internally), then - * calls {@code response.end()} normally so the HTTP client gets a response. + * HttpServerResponseImpl.handleException is invoked by Vert.x on non-CLOSED_EXCEPTION + * I/O failures of the response. Neither endHandler nor bodyEndHandler fires on this path, so the + * route-handler span would leak without an exception handler registered. The route handler here + * fires handleException directly via ResponseExceptionFiringHelper (the package-private method + * Vert.x itself uses internally), then calls response.end() normally so the HTTP client gets a + * response. */ class RouteHandlerExceptionHandlerTest extends AbstractInstrumentationTest { @@ -108,11 +107,6 @@ void exceptionHandlerFinishesRouteHandlerSpan() throws Exception { conn.disconnect(); } - // Strict-mode trace writes only publish when every span in the trace has finished. - // If response.exceptionHandler did not finish the route-handler span, assertTraces - // would time out waiting for the trace to flush. - // Span operation names are stored as UTF8BytesString, whose equals() rejects String - // arguments, so match via a quoted Pattern instead of the String overload. // The netty.request span is marked as errored because the route handler ends with // HTTP 500; the route-handler span is finished by our exception handler before // setStatusCode(500), so it sees status=200 (default) and is not errored. diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java index 9de8b1b7af0..8e8c52f704b 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java @@ -26,14 +26,13 @@ import org.junit.jupiter.api.Test; /** - * Regression test for the vertx-web 3.x route-handler span lifecycle on the {@code - * response.sendFile(...)} path. + * Regression test for the vertx-web 3.x route-handler span lifecycle on the response.sendFile(...) path. * - *

{@code HttpServerResponseImpl.doSendFile} (vertx-core 3.x) only invokes {@code bodyEndHandler} - * after the file is written; it never invokes {@code endHandler}. With only the {@code endHandler} - * registration (pre-fix), the {@code vertx.route-handler} span never finishes on this path, the - * trace fails to flush, and {@code assertTraces} times out. With the fallback {@code - * addBodyEndHandler} registration, the span finishes on every response-end path. + * HttpServerResponseImpl.doSendFile (vertx-core 3.x) only invokes bodyEndHandler + * after the file is written; it never invokes endHandler. With only the endHandler registration + * (pre-fix), the vertx.route-handler span never finishes on this path, the trace fails to flush, + * and assertTraces times out. With the fallback addBodyEndHandler registration, the span finishes + * on every response-end path. */ class RouteHandlerSendFileTest extends AbstractInstrumentationTest { @@ -106,11 +105,8 @@ void sendFileFinishesRouteHandlerSpan() throws Exception { assertEquals("vertx sendFile payload", reader.readLine()); } - // Strict-mode trace writes only publish a trace when every span in it has finished. // Pre-fix: the route-handler span never finishes on the sendFile path, so the trace // is never published and assertTraces times out waiting for the trace to flush. - // Span operation names are stored as UTF8BytesString, whose equals() rejects String - // arguments, so match via a quoted Pattern instead of the String overload. assertTraces( trace( SORT_BY_START_TIME, diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle index 08c19f83656..ee305e1e172 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/build.gradle @@ -45,11 +45,6 @@ dependencies { testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') - // Make JUnit Jupiter API explicit for the Java JUnit 5 tests in this module - // (RouteHandlerExceptionHandlerTest). The shared gradle/java_deps.gradle only - // declares JUnit 5 as testRuntimeOnly; otherwise we'd be relying on Spock 2.x's - // transitive junit-jupiter-api on testCompileClasspath, which is implicit and - // brittle. testImplementation libs.junit.jupiter testRuntimeOnly project(':dd-java-agent:instrumentation:jackson-core:jackson-core-common') diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java index be8c55eff6f..0ce07e64fd4 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java @@ -3,10 +3,10 @@ import io.vertx.core.http.HttpServerResponse; /** - * Test-side bridge that fires the package-private {@code Http1xServerResponse.handleException} on a - * Vert.x 4.x server response. Used by {@code server.RouteHandlerExceptionHandlerTest} to - * deterministically reproduce the non-{@code CLOSED_EXCEPTION} I/O-failure path that Vert.x exposes - * via {@code response.exceptionHandler(...)}. + * Test-side bridge that fires the package-private Http1xServerResponse.handleException on a + * Vert.x 4.x server response. Used by server.RouteHandlerExceptionHandlerTest to + * deterministically reproduce the non-CLOSED_EXCEPTION I/O-failure path that Vert.x exposes + * via response.exceptionHandler(...). */ public final class ResponseExceptionFiringHelper { private ResponseExceptionFiringHelper() {} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java index e717c6e97c4..c4391d26eb6 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java @@ -22,17 +22,16 @@ import org.junit.jupiter.api.Test; /** - * Regression test for the vertx-web 4.x route-handler span lifecycle on the {@code - * response.exceptionHandler} path. + * Regression test for the vertx-web 4.x route-handler span lifecycle on the response.exceptionHandler path. * - *

{@code Http1xServerResponse.handleException} is invoked by Vert.x on non-{@code - * CLOSED_EXCEPTION} I/O failures of the response. Without {@code RoutingContext.addEndHandler(...)} - * registered, only the wrapped {@code response.endHandler} could finish the route-handler span — - * and that hook does not fire on the exception path. With the {@code addEndHandler} fallback in - * {@code RouteHandlerWrapper}, the routing context's internal exception handler fires our - * completion callback regardless of which response hook surfaces the error. The route handler here - * fires {@code handleException} directly via {@link ResponseExceptionFiringHelper}, then calls - * {@code response.end()} normally so the HTTP client gets a response. + * Http1xServerResponse.handleException is invoked by Vert.x on non-CLOSED_EXCEPTION + * I/O failures of the response. Without RoutingContext.addEndHandler(...) registered, only the + * wrapped response.endHandler could finish the route-handler span — and that hook does not fire + * on the exception path. With the addEndHandler fallback in RouteHandlerWrapper, the routing + * context's internal exception handler fires our completion callback regardless of which response + * hook surfaces the error. The route handler here fires handleException directly via + * ResponseExceptionFiringHelper, then calls response.end() normally so the HTTP client gets a + * response. */ class RouteHandlerExceptionHandlerTest extends AbstractInstrumentationTest { @@ -110,11 +109,8 @@ void exceptionHandlerFinishesRouteHandlerSpan() throws Exception { conn.disconnect(); } - // Strict-mode trace writes only publish when every span in the trace has finished. // If addEndHandler did not finish the route-handler span, assertTraces would time out // waiting for the trace to flush. - // Span operation names are stored as UTF8BytesString, whose equals() rejects String - // arguments, so match via a quoted Pattern instead of the String overload. // The netty.request span is marked as errored because the route handler ends with // HTTP 500; the route-handler span is finished by our addEndHandler callback before // setStatusCode(500), so it sees status=200 (default) and is not errored. From 7b0e6860922b260d546974bba2d2dc5a99133ce2 Mon Sep 17 00:00:00 2001 From: Rithika Narayan Date: Mon, 18 May 2026 16:40:25 -0400 Subject: [PATCH 9/9] spotless --- .../http/impl/ResponseExceptionFiringHelper.java | 8 ++++---- .../server/RouteHandlerExceptionHandlerTest.java | 7 ++++--- .../java/server/RouteHandlerSendFileTest.java | 13 +++++++------ .../http/impl/ResponseExceptionFiringHelper.java | 8 ++++---- .../server/RouteHandlerExceptionHandlerTest.java | 15 ++++++++------- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java index 13e2fe7def0..5c581365de5 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java @@ -3,10 +3,10 @@ import io.vertx.core.http.HttpServerResponse; /** - * Test-side bridge that fires the package-private HttpServerResponseImpl.handleException on - * a Vert.x 3.x server response. Used by server.RouteHandlerExceptionHandlerTest to - * deterministically reproduce the non-CLOSED_EXCEPTION I/O-failure path that Vert.x exposes - * via response.exceptionHandler(...). + * Test-side bridge that fires the package-private HttpServerResponseImpl.handleException on a + * Vert.x 3.x server response. Used by server.RouteHandlerExceptionHandlerTest to deterministically + * reproduce the non-CLOSED_EXCEPTION I/O-failure path that Vert.x exposes via + * response.exceptionHandler(...). */ public final class ResponseExceptionFiringHelper { private ResponseExceptionFiringHelper() {} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java index 897458d4e49..3892646adcf 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerExceptionHandlerTest.java @@ -22,10 +22,11 @@ import org.junit.jupiter.api.Test; /** - * Regression test for the vertx-web 3.x route-handler span lifecycle on the response.exceptionHandler path. + * Regression test for the vertx-web 3.x route-handler span lifecycle on the + * response.exceptionHandler path. * - * HttpServerResponseImpl.handleException is invoked by Vert.x on non-CLOSED_EXCEPTION - * I/O failures of the response. Neither endHandler nor bodyEndHandler fires on this path, so the + *

HttpServerResponseImpl.handleException is invoked by Vert.x on non-CLOSED_EXCEPTION I/O + * failures of the response. Neither endHandler nor bodyEndHandler fires on this path, so the * route-handler span would leak without an exception handler registered. The route handler here * fires handleException directly via ResponseExceptionFiringHelper (the package-private method * Vert.x itself uses internally), then calls response.end() normally so the HTTP client gets a diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java index 8e8c52f704b..06102063a1b 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/java/server/RouteHandlerSendFileTest.java @@ -26,13 +26,14 @@ import org.junit.jupiter.api.Test; /** - * Regression test for the vertx-web 3.x route-handler span lifecycle on the response.sendFile(...) path. + * Regression test for the vertx-web 3.x route-handler span lifecycle on the response.sendFile(...) + * path. * - * HttpServerResponseImpl.doSendFile (vertx-core 3.x) only invokes bodyEndHandler - * after the file is written; it never invokes endHandler. With only the endHandler registration - * (pre-fix), the vertx.route-handler span never finishes on this path, the trace fails to flush, - * and assertTraces times out. With the fallback addBodyEndHandler registration, the span finishes - * on every response-end path. + *

HttpServerResponseImpl.doSendFile (vertx-core 3.x) only invokes bodyEndHandler after the file + * is written; it never invokes endHandler. With only the endHandler registration (pre-fix), the + * vertx.route-handler span never finishes on this path, the trace fails to flush, and assertTraces + * times out. With the fallback addBodyEndHandler registration, the span finishes on every + * response-end path. */ class RouteHandlerSendFileTest extends AbstractInstrumentationTest { diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java index 0ce07e64fd4..4754c38843e 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/io/vertx/core/http/impl/ResponseExceptionFiringHelper.java @@ -3,10 +3,10 @@ import io.vertx.core.http.HttpServerResponse; /** - * Test-side bridge that fires the package-private Http1xServerResponse.handleException on a - * Vert.x 4.x server response. Used by server.RouteHandlerExceptionHandlerTest to - * deterministically reproduce the non-CLOSED_EXCEPTION I/O-failure path that Vert.x exposes - * via response.exceptionHandler(...). + * Test-side bridge that fires the package-private Http1xServerResponse.handleException on a Vert.x + * 4.x server response. Used by server.RouteHandlerExceptionHandlerTest to deterministically + * reproduce the non-CLOSED_EXCEPTION I/O-failure path that Vert.x exposes via + * response.exceptionHandler(...). */ public final class ResponseExceptionFiringHelper { private ResponseExceptionFiringHelper() {} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java index c4391d26eb6..fb91f77a4b7 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/java/server/RouteHandlerExceptionHandlerTest.java @@ -22,14 +22,15 @@ import org.junit.jupiter.api.Test; /** - * Regression test for the vertx-web 4.x route-handler span lifecycle on the response.exceptionHandler path. + * Regression test for the vertx-web 4.x route-handler span lifecycle on the + * response.exceptionHandler path. * - * Http1xServerResponse.handleException is invoked by Vert.x on non-CLOSED_EXCEPTION - * I/O failures of the response. Without RoutingContext.addEndHandler(...) registered, only the - * wrapped response.endHandler could finish the route-handler span — and that hook does not fire - * on the exception path. With the addEndHandler fallback in RouteHandlerWrapper, the routing - * context's internal exception handler fires our completion callback regardless of which response - * hook surfaces the error. The route handler here fires handleException directly via + *

Http1xServerResponse.handleException is invoked by Vert.x on non-CLOSED_EXCEPTION I/O failures + * of the response. Without RoutingContext.addEndHandler(...) registered, only the wrapped + * response.endHandler could finish the route-handler span — and that hook does not fire on the + * exception path. With the addEndHandler fallback in RouteHandlerWrapper, the routing context's + * internal exception handler fires our completion callback regardless of which response hook + * surfaces the error. The route handler here fires handleException directly via * ResponseExceptionFiringHelper, then calls response.end() normally so the HTTP client gets a * response. */