'Spring Boot Upgrade from 2.3.x -> 2.6.x Breaks mvc forward:
After upgrading From Spring Boot 2.3.x to 2.6.x Forwarding MVC REST Calls doesn't work.
We have a controller for /health
that returns forward:/actuator/health/
.
forward:/actuator/health/
fails in the controller, but localhost:8080/actuator/health/
works just fine in my browser when hit directly.
We have the following controller:
@Controller
public class HealthRestController implements HealthAPI
{
@Value("forward:/actuator/health/")
String forwardString;
@CrossOrigin
@GetMapping(value = {"/health"}, produces = "application/json")
public String getHealth() { return forwardString; }
@GetMapping(value = {"/health_secure"}, produces = "application/json")
public String getHealthSecure() { return forwardString; }
}
/health
does not require authentication.
@Override
public void configure(WebSecurity web) throws Exception
{ //other urls removed, for simplicity
web.ignoring()
.antMatchers("/health",
"/actuator/health",
"/actuator/prometheus",
"/cloudfoundryapplication",
"/actuator/cloudfoundryapplication",
"/cloudfoundryapplication/**");
}
But when I hit /health
in My Web Browser, I get the following errors:
May 10, 2022 10:44:28 AM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-05-10 10:44:28 INFO DispatcherServlet:525 - - Initializing Servlet 'dispatcherServlet'
2022-05-10 10:44:28 INFO DispatcherServlet:547 - - Completed initialization in 2 ms
May 10, 2022 10:44:28 AM org.apache.catalina.core.ApplicationDispatcher invoke
SEVERE: Servlet.service() for servlet [dispatcherServlet] threw exception
javax.servlet.ServletException: Could not resolve view with name 'forward:/actuator/health/' in servlet with name 'dispatcherServlet'
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1380)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1145)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1084)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:228)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:711)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:459)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:385)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:313)
at com.allstate.d3.sh.commons.config.CloudMetricsConfig$2.service(CloudMetricsConfig.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:228)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1723)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
May 10, 2022 10:44:28 AM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [health] in context with path [/health] threw exception [Could not resolve view with name 'forward:/actuator/health/' in servlet with name 'dispatcherServlet'] with root cause
javax.servlet.ServletException: Could not resolve view with name 'forward:/actuator/health/' in servlet with name 'dispatcherServlet'
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1380)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1145)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1084)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:228)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:711)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:459)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:385)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:313)
at com.allstate.d3.sh.commons.config.CloudMetricsConfig$2.service(CloudMetricsConfig.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:228)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1723)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
So what are we missing? why doesn't forward:
work?
UPDATE
Dependency snippet from build.gradle with Spring dependencies
buildscript
{
ext { springBootVersion = '2.6.7' }
//other stuff here
//skip repositories
dependencies
{
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
//other non-spring dependencies here
}
}
//dependencies
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework:spring-context')
implementation('org.springframework:spring-expression')
implementation('org.springframework:spring-beans')
implementation('org.springframework:spring-core')
implementation('org.springframework.boot:spring-boot-starter-webflux')
implementation("org.springframework.cloud:spring-cloud-starter-vault-config:3.1.0")
implementation('org.springframework.security:spring-security-web')
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.security.oauth:spring-security-oauth2:${springSecurityOauth2Version}")
implementation "org.springframework.cloud:spring-cloud-spring-service-connector:${springCloudConnectorVersion}"
implementation "org.springframework.cloud:spring-cloud-cloudfoundry-connector:${springCloudConnectorVersion}"
//websocket
implementation("org.springframework.boot:spring-boot-starter-websocket")
// cache
implementation 'org.springframework:spring-context'
implementation 'org.springframework:spring-context-support'
implementation 'org.springdoc:springdoc-openapi-ui:1.6.1'
implementation("org.springframework.security:spring-security-jwt:${springSecurityJwtVersion}")
application context path is set in application.yml
server:
servlet:
contextPath: /exe/v2
Paths are exposed outside of the context path like so:
@Configuration
public class CloudMetricsConfig
{
@Bean
public TomcatServletWebServerFactory servletWebServerFactory()
{
return new TomcatServletWebServerFactory()
{
@Override
protected void prepareContext(Host host,
ServletContextInitializer[] initializers)
{
super.prepareContext(host, initializers);
addContext(host, "/cloudfoundryapplication", getContextPath(),
"cloudfoundry");
addContext(host, "/actuator/prometheus", getContextPath(),
"prometheus");
addContext(host, "/actuator/health", getContextPath(),
"actuatorHealth");
addContext(host, "/health", getContextPath(),
"health");
}
};
}
private void addContext(Host host, String path, String contextPath,
String servletName)
{
StandardContext child = new StandardContext();
child.addLifecycleListener(new Tomcat.FixContextListener());
child.setPath(path);
ServletContainerInitializer initializer =
getServletContextInitializer(contextPath, servletName, path);
child.addServletContainerInitializer(initializer, Collections.emptySet());
child.setCrossContext(true);
host.addChild(child);
}
private ServletContainerInitializer getServletContextInitializer(String contextPath,
String servletName,
String path)
{
return (c, context) ->
{
Servlet servlet = new GenericServlet()
{
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
ServletContext context = req.getServletContext().getContext(contextPath);
context.getRequestDispatcher(path).forward(req, res);
}
};
context.addServlet(servletName, servlet)//.addMapping(path);
.addMapping("/*");
};
}
Solution 1:[1]
Just checked with Spring Boot 2.6.7 - works as expected.
org.springframework.web.servlet.view.UrlBasedViewResolver#FORWARD_URL_PREFIX
(which is the string "forward:"
) is the part of spring-webmvc
module. Check that you have this in your project's dependencies, with the version that corresponds to the Spring/Boot version you use.
Solution 2:[2]
Per @dekkard 's answer: https://stackoverflow.com/a/72192299/659354
I upgraded from Spring Boot 2.6.2 -> 2.6.7 to minimize version overrides
for more secure versions. And verified the presence of spring-mvc
The final piece of the puzzle came from following the link in @RAHULBHOITE 's answer
Spring boot single page application - forward every request to index.html
I removed @EnableWebMvc
from one of my configuration classes.
Now the /health
works. All my integration tests still pass, and manual postman testing for the REST endpoints still works.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | dekkard |
Solution 2 | Raystorm |