From d5e91fe1086e006f71691a2b993085c56f4c05ca Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 29 Sep 2026 15:15:54 -0400 Subject: [PATCH] Fix NPE when Spring messaging handler is invoked with a null message ContextPropagationAdvice extracted context from the handler's message argument without a null check, and SpringMessageExtractAdapter dereferenced its carrier unguarded, so a null message threw an NPE on every invocation that had no active span. Skip extraction when the message is null and make the adapter tolerate a null carrier. Co-Authored-By: Claude Sonnet 5.5 --- .../SpringMessageExtractAdapter.java | 3 + .../SpringMessageHandlerInstrumentation.java | 2 +- .../SpringMessageExtractAdapterTest.java | 58 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/test/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapterTest.java diff --git a/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapter.java b/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapter.java index 2d6e1befd70..74bcd39a922 100644 --- a/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapter.java +++ b/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapter.java @@ -34,6 +34,9 @@ public String apply(String key) { @Override public void forEachKey(Message carrier, AgentPropagation.KeyClassifier classifier) { + if (carrier == null) { + return; + } final MessageHeaders messageHeaders = carrier.getHeaders(); if (messageHeaders == null || messageHeaders.isEmpty()) { return; diff --git a/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageHandlerInstrumentation.java b/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageHandlerInstrumentation.java index a42e4bfdfd6..a5af04b1b1c 100644 --- a/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/main/java/datadog/trace/instrumentation/springmessaging/SpringMessageHandlerInstrumentation.java @@ -65,7 +65,7 @@ public static class ContextPropagationAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter( @Advice.Argument(0) Message message, @Advice.Local("ctxScope") ContextScope scope) { - if (activeSpan() == null) { + if (message != null && activeSpan() == null) { // no local active span, so extract from message to avoid disconnected trace scope = defaultPropagator().extract(rootContext(), message, GETTER).attach(); } diff --git a/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/test/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapterTest.java b/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/test/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapterTest.java new file mode 100644 index 00000000000..cf16a9fd7a7 --- /dev/null +++ b/dd-java-agent/instrumentation/spring/spring-messaging-4.0/src/test/java/datadog/trace/instrumentation/springmessaging/SpringMessageExtractAdapterTest.java @@ -0,0 +1,58 @@ +package datadog.trace.instrumentation.springmessaging; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.support.GenericMessage; + +class SpringMessageExtractAdapterTest { + + @Test + void ignoresNullMessage() { + List seen = new ArrayList<>(); + + SpringMessageExtractAdapter.GETTER.forEachKey(null, collectInto(seen)); + + assertEquals(emptyList(), seen); + } + + @Test + void ignoresMessageWithoutHeaders() { + List seen = new ArrayList<>(); + + SpringMessageExtractAdapter.GETTER.forEachKey( + new GenericMessage<>("payload", new HashMap()), collectInto(seen)); + + assertEquals(emptyList(), seen); + } + + @Test + void normalizesKeysAndSkipsNonStringHeaders() { + Map headers = new HashMap<>(); + headers.put("X-Datadog-Trace-Id", "123"); + headers.put("AWSTraceHeader", "Root=1-abc"); + headers.put("not-a-string", 42); + List seen = new ArrayList<>(); + + SpringMessageExtractAdapter.GETTER.forEachKey( + new GenericMessage<>("payload", headers), collectInto(seen)); + + // header order is not guaranteed, so compare as sorted entries + seen.sort(String::compareTo); + assertEquals(asList("x-amzn-trace-id=Root=1-abc", "x-datadog-trace-id=123"), seen); + } + + private static AgentPropagation.KeyClassifier collectInto(List seen) { + return (key, value) -> { + seen.add(key + "=" + value); + return true; + }; + } +}