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("/*");
};
}
CodePudding user response:
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.
CodePudding user response:
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.