From 6858d091e90750002427f17fccfd37b0afd06cea Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 8 Oct 2026 15:20:10 -0400 Subject: [PATCH] Skip storing the dispatcher target when the servlet context returns no dispatcher ServletContext.getRequestDispatcher/getNamedDispatcher return null for an unknown path or servlet name (e.g. Tomcat's ApplicationContext). The exit advice then used that null dispatcher as a context store key, which throws an NPE in the weak-map fallback. Co-Authored-By: Claude Opus 5.5 --- .../ServletContextInstrumentation.java | 4 + .../ServletContextNullDispatcherTest.java | 142 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/test/java/ServletContextNullDispatcherTest.java diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java index b732436003a..de92d9b2b29 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java @@ -55,6 +55,10 @@ public static class RequestDispatcherTargetAdvice { public static void saveTarget( @Advice.Argument(0) final String target, @Advice.Return final RequestDispatcher dispatcher) { + if (dispatcher == null) { + // containers return no dispatcher for an unknown path or servlet name + return; + } InstrumentationContext.get(RequestDispatcher.class, String.class).put(dispatcher, target); } } diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/test/java/ServletContextNullDispatcherTest.java b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/test/java/ServletContextNullDispatcherTest.java new file mode 100644 index 00000000000..c8bddea3cdb --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/test/java/ServletContextNullDispatcherTest.java @@ -0,0 +1,142 @@ +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.Set; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import org.junit.jupiter.api.Test; + +/** + * Servlet containers return a {@code null} dispatcher for unknown paths or servlet names (e.g. + * Tomcat's {@code ApplicationContext}); the instrumentation must not try to store context against a + * {@code null} key. Instrumentation errors fail the test via {@link AbstractInstrumentationTest}. + */ +class ServletContextNullDispatcherTest extends AbstractInstrumentationTest { + + @Test + void getRequestDispatcherReturningNull() { + assertNull(new NullDispatcherContext().getRequestDispatcher("unknown")); + } + + @Test + void getNamedDispatcherReturningNull() { + assertNull(new NullDispatcherContext().getNamedDispatcher("unknown")); + } + + /** {@link ServletContext} that has no dispatchers, like Tomcat for an unknown target. */ + static class NullDispatcherContext implements ServletContext { + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return null; + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + return null; + } + + @Override + public ServletContext getContext(String uripath) { + return null; + } + + @Override + public int getMajorVersion() { + return 2; + } + + @Override + public int getMinorVersion() { + return 3; + } + + @Override + public String getMimeType(String file) { + return null; + } + + @Override + public Set getResourcePaths(String path) { + return null; + } + + @Override + public URL getResource(String path) { + return null; + } + + @Override + public InputStream getResourceAsStream(String path) { + return null; + } + + @Override + public Servlet getServlet(String name) { + return null; + } + + @Override + public Enumeration getServlets() { + return null; + } + + @Override + public Enumeration getServletNames() { + return null; + } + + @Override + public void log(String msg) {} + + @Override + public void log(Exception exception, String msg) {} + + @Override + public void log(String message, Throwable throwable) {} + + @Override + public String getRealPath(String path) { + return null; + } + + @Override + public String getServerInfo() { + return null; + } + + @Override + public String getInitParameter(String name) { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public void setAttribute(String name, Object object) {} + + @Override + public void removeAttribute(String name) {} + + @Override + public String getServletContextName() { + return null; + } + } +}