Skip to content

Commit 3d736db

Browse files
author
dmitrii
committed
Set Spring MVC context when RequestContextFilter is absent
The Spring MVC context was only created by wrapping RequestContextFilter#doFilterInternal. WebMvcAutoConfiguration does not register that filter when a RequestContextListener bean is present, so the context was never set: controllers logged "Received Spring Annotations, but no context set" on every request and input/IP/UA inspection did not run. Wrap FrameworkServlet#processRequest, which runs for every DispatcherServlet request regardless of the filter chain, and create the context when it is missing. A per-request marker set by the filter prevents double creation on the normal path.
1 parent 8de3298 commit 3d736db

3 files changed

Lines changed: 111 additions & 0 deletions

File tree

agent/src/main/java/dev/aikido/agent/Wrappers.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import dev.aikido.agent.wrappers.spring.SpringWebfluxWrapper;
1010
import dev.aikido.agent.wrappers.spring.SpringControllerWrapper;
1111
import dev.aikido.agent.wrappers.spring.SpringMVCJakartaWrapper;
12+
import dev.aikido.agent.wrappers.spring.SpringMVCDispatcherWrapper;
1213

1314
import java.util.Arrays;
1415
import java.util.List;
@@ -18,6 +19,7 @@ private Wrappers() {}
1819
public static final List<Wrapper> WRAPPERS = Arrays.asList(
1920
new PostgresWrapper(),
2021
new SpringMVCJakartaWrapper(),
22+
new SpringMVCDispatcherWrapper(),
2123
new SpringMVCJavaxWrapper(),
2224
new SpringWebfluxWrapper(),
2325
new SpringControllerWrapper(),
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package dev.aikido.agent.wrappers.spring;
2+
3+
import dev.aikido.agent.wrappers.Wrapper;
4+
import dev.aikido.agent_api.collectors.WebRequestCollector;
5+
import dev.aikido.agent_api.collectors.WebResponseCollector;
6+
import dev.aikido.agent_api.context.ContextObject;
7+
import dev.aikido.agent_api.context.SpringMVCContextObject;
8+
import dev.aikido.agent_api.helpers.logging.LogManager;
9+
import dev.aikido.agent_api.helpers.logging.Logger;
10+
import jakarta.servlet.http.Cookie;
11+
import jakarta.servlet.http.HttpServletRequest;
12+
import jakarta.servlet.http.HttpServletResponse;
13+
import net.bytebuddy.asm.Advice;
14+
import net.bytebuddy.description.method.MethodDescription;
15+
import net.bytebuddy.description.type.TypeDescription;
16+
import net.bytebuddy.matcher.ElementMatcher;
17+
import net.bytebuddy.matcher.ElementMatchers;
18+
19+
import java.lang.reflect.Executable;
20+
import java.util.ArrayList;
21+
import java.util.Enumeration;
22+
import java.util.HashMap;
23+
import java.util.List;
24+
25+
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
26+
import static net.bytebuddy.matcher.ElementMatchers.nameContains;
27+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
28+
29+
// Fallback context creator for Spring MVC: wraps FrameworkServlet#processRequest (runs for every
30+
// DispatcherServlet request) and creates the context when RequestContextFilter is absent from the chain.
31+
public class SpringMVCDispatcherWrapper implements Wrapper {
32+
public static final Logger logger = LogManager.getLogger(SpringMVCDispatcherWrapper.class);
33+
34+
@Override
35+
public String getName() {
36+
return SpringMVCDispatcherAdvice.class.getName();
37+
}
38+
39+
@Override
40+
public ElementMatcher<? super MethodDescription> getMatcher() {
41+
return ElementMatchers.nameContainsIgnoreCase("processRequest")
42+
.and(takesArgument(0, nameContains("jakarta")))
43+
.and(takesArgument(1, nameContains("jakarta")));
44+
}
45+
46+
@Override
47+
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
48+
return nameContains("org.springframework.web.servlet.FrameworkServlet");
49+
}
50+
51+
public static class SpringMVCDispatcherAdvice {
52+
public record SkipOnWrapper(HttpServletResponse response) {}
53+
54+
@Advice.OnMethodEnter(skipOn = SkipOnWrapper.class, suppress = Throwable.class)
55+
public static Object interceptOnEnter(
56+
@Advice.Origin Executable method,
57+
@Advice.Argument(value = 0, typing = DYNAMIC, optional = true) HttpServletRequest request,
58+
@Advice.Argument(value = 1, typing = DYNAMIC, optional = true) HttpServletResponse response) throws Throwable {
59+
if (request == null) {
60+
return null;
61+
}
62+
// Per-request marker (not Context.get()) since pooled threads keep a stale context between requests.
63+
if (request.getAttribute("dev.aikido.zen.springContextCreated") != null) {
64+
return null;
65+
}
66+
HashMap<String, Enumeration<String>> headersMap = new HashMap<>();
67+
Enumeration<String> headerNames = request.getHeaderNames();
68+
while (headerNames != null && headerNames.hasMoreElements()) {
69+
String headerName = headerNames.nextElement();
70+
Enumeration<String> headerValue = request.getHeaders(headerName);
71+
headersMap.put(headerName, headerValue);
72+
}
73+
HashMap<String, List<String>> cookiesMap = new HashMap<>();
74+
Cookie[] cookies = request.getCookies();
75+
if (cookies != null) {
76+
for (Cookie cookie : cookies) {
77+
if (!cookiesMap.containsKey(cookie.getName())) {
78+
cookiesMap.put(cookie.getName(), new ArrayList<>());
79+
}
80+
cookiesMap.get(cookie.getName()).add(cookie.getValue());
81+
}
82+
}
83+
84+
ContextObject contextObject = new SpringMVCContextObject(
85+
request.getMethod(), request.getRequestURL(), request.getRemoteAddr(),
86+
request.getParameterMap(), cookiesMap, headersMap, request.getQueryString()
87+
);
88+
89+
WebRequestCollector.Res res = WebRequestCollector.report(contextObject);
90+
if (res != null && response != null) {
91+
response.setStatus(res.status());
92+
response.setContentType("text/plain");
93+
response.getWriter().write(res.msg());
94+
return new SkipOnWrapper(response);
95+
}
96+
return response;
97+
}
98+
99+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
100+
public static void interceptOnExit(@Advice.Enter Object response) {
101+
if (response instanceof HttpServletResponse httpServletResponse) {
102+
WebResponseCollector.report(httpServletResponse.getStatus());
103+
}
104+
}
105+
}
106+
}

agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCJakartaWrapper.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ public static Object interceptOnEnter(
9090
request.getParameterMap(), cookiesMap, headersMap, request.getQueryString()
9191
);
9292

93+
// Marker read by SpringMVCDispatcherWrapper so the fallback does not re-create this context.
94+
request.setAttribute("dev.aikido.zen.springContextCreated", Boolean.TRUE);
95+
9396
// Write a new response:
9497
WebRequestCollector.Res res = WebRequestCollector.report(contextObject);
9598
if (res != null) {

0 commit comments

Comments
 (0)