Home > OS >  Spring Boot Upgrade from 2.3.x -> 2.6.x Breaks mvc forward:
Spring Boot Upgrade from 2.3.x -> 2.6.x Breaks mvc forward:

Time:05-14

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.

  • Related