The Problem

Using Springframework v6.2.6 (not spring boot) and Tomcat v11.0.6 (not embedded) I cannot use the ShallowEtagHeaderFiler on a resource that is internally forwarded. I have several url paths that should result in the index.html page being served (single page application style), and I use server-side forwarding so the url displayed to the user remains the same. After the forward happens, the response content is missing (blank page), and the ETag header isn't present, despite a 200 status code. I also see this in the tomcat access log

org.apache.catalina.core.ApplicationDispatcher.forward Disabling the response for further output

Specifically, I think this happens when a HttpServletResponseWrapper (like what is used by ShallowEtagHeaderFilter) is active in the filter chain and I perform a RequestDispatcher.forward() (e.g., via Spring MVC's InternalResourceViewResolver) to a resource. After the forward, the output can't be written to for the Etag itself, and also for the contents that were cached.

Forwarding works as expected using Spring v5.3.27 and Tomcat 9.0.102

Sample Setup

TestController

@Controller
public class TestController {
    @RequestMaping(value = {"/", "/admin/, "/admin", "/dev", "/dev/"})
    public String handleRootRequest() {
       return "/resources/index.html";
    }
}

MvcConfiguration

@Configuration
@EnableWebMvc
public class TestMvcConfiguration implements WebMvcConfigurer

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
            .addResourceLocations("/resources/")
            .setCachePeriod(0);
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParmeter(true)
            .useRegisteredExtensionsOnly(true)
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("html", MediaType.TEXT_HTMNL);
     }

     // Have tried this instead of the controller endpoint, but same result
     // @override
    // public void addViewControllers(ViewControllerRegistry registry) {
    //    registry.addViewController("/").setViewName("forward:/resources/index.html");
    // }

Snippet of applicable WebApplicationInitializer

public class WebApInitializer implements WebApplicationInitializer {

    @Override
     public void onStartup(ServletContext servletContext) throws ServletException {
         try (AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
              AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext(); ) {
              rootContext.register(AppConfig.class);
              servletContext.addListener(new ContextLoaderListener(rootContext));

               // filters, but the etag filer is the last one
              FilterRegistration.Dynamic etagFilter = servletContext.addFilter("shallowEtagHeaderFilter", new ShallowETagHeaderFilter());
               etagFilter.setAsyncSupported(true);
               etagFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ASYNC), true, "/*");

                dispatcherContext.register(MvcConfig.class);
                DispatcherServlet dispatcherSrv = new DispatcherServlet(dispatcherContext);
                ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", dispatcherSrv);
                dispatcher.addLoadOnStartup(1);
                dispatcher.addMapping("/");
                dispatcher.setAsyncSupported(true);
          }
     }
}

Create a generic ApplicationConfiguration Create a generic index.html file and put it in src/main/webapps/resources. Create a war file and deploy it to Tomcat. Attempt to go to localhost:8080/testApp (or whatever the root context of the war file is) You won't get the contents of the index.html, only a blank page.

Workaround

If I stream my index.html file directly from the endpoint in the Controller, thereby bypassing the forwarding by the mvc internal resource handler, the content of the index.html appears and the Etag header is present The following code in the Controller (the MvcConfiguration code is being bypassed) works

@Controller
public class TestController {
    @RequestMaping(value = {"/", "/admin/, "/admin", "/dev", "/dev/"})
    public void handleRootRequest(HttpServletRequest request, HttpServletResponse response) {
        response.setContentType(MediaType.TEXT_HTML_VALUE);
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);

        ServletContext servletContext = request.getServletContext();
        String indexPath = "/resources/index.html";
        InputStream is = servletContext.getResourceAsStream(indexPath);

        if (is != null) {
            try (OutputStream os = response.getOutputStream()) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = is.read(buffer)) != -1) {
                    os.write(buffer, 0, bytesRead);
                }
            } finally {
                is.close(); 
            }
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}