Search code examples
springspring-bootspring-mvcservlet-filtersundertow

SpringBoot 2 - OncePerRequestFilter - modify response Headers after processing of Controller


Hello I want to modify some of my API's response Headers after I have completed processing (executed logic) and have concluded with an HTTP status code.

For example if the response is 404, then include specific for example Cache-Control Headers example dont cache, or something like that.

I have already 2 OncePerRequestFilter registered, which work fine - but obviously I can not do logic - once the processing is complete. The CacheControlFilter already has logic that adds by default some Cache-Control headers - e.g cache for 15 sec etc. It seems though that this happens (the addition of headers on the response) on a very early stage of the dispatch and when it reaches to the phase of executing the actual Controller/Endpoint and there is an exception or Error that obviously is going to be handled by an advice etc, I can not mutate these already existing headers- that were already added by the filter.


  @Bean
  public FilterRegistrationBean filterOne() {
    Filter filter = new FilterOne();
    return createFilter(filter, "FilterOne",List.of("/*"));
  }

  @Bean
  public FilterRegistrationBean cacheControlFilter() {
    Filter filter = new CacheControlFilter();
    return createFilter(filter, "CacheControlFilter", List.of("/*"));
  }

  private FilterRegistrationBean createFilter(Filter aFilter, String filterName,
      List<String> urlPatterns) {
    FilterRegistrationBean filterRegBean = new FilterRegistrationBean(aFilter);
    filterRegBean.addUrlPatterns(urlPatterns.toArray(new String[0]));
    filterRegBean.setName(filterName);
    filterRegBean.setEnabled(true);
    filterRegBean.setAsyncSupported(true);
    return filterRegBean;
  }

I have already tried, to add an HttpServletResponseWrapper as indicated on these post here and here on the CacheControlFilter but it does not seem to work. I have also seen a similar S.O thread here.

HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(response) {
   @Override
   public void setStatus(int sc) {
      super.setStatus(sc);
      handleStatus(sc);
  }

 @Override
 @SuppressWarnings("deprecation")
 public void setStatus(int sc, String sm) {
     super.setStatus(sc, sm);
     handleStatus(sc);
 }

 @Override
 public void sendError(int sc, String msg) throws IOException {
     super.sendError(sc, msg);
     handleStatus(sc);
  }

 @Override
 public void sendError(int sc) throws IOException {
     super.sendError(sc);
     handleStatus(sc);
 }

 private void handleStatus(int code) {
    if(code == 404)
         addHeader("Cache-Control, "xxx");
 }

};

But the code is not executed at all! So I want to manipulate the Cache-Control headers on the second filter only after though the processing is complete and I am ready to return a response.

I am not sure if the fact that I also have, doing some clean up and setting responses upon errors - mixes things up!

@ControllerAdvice
@Slf4j
public class GlobalErrorHandler

Update: As a note, when my Controller is throwing an Exception or Error, the above GlobalErrorHandler is invoked and there I execute a special handling, returning an error response. What I see though is that magically the response has already the default headers populated by the Filter (CacheControlFilter). So it ends up being a bit weird, I add extra logic,to change the control header and I end up with a response that has the same header 2 times (1 with the value set by the CacheControlFilter and then any special value I am trying to override on the ControllerAdvice

Any tips or help appreciated thanks! I am using Spring Boot 2.1.2 with Undertow as my underlying servlet container.


Solution

  • The link you mentioned says that cannot get the status code or modify the headers in ResponseBodyAdvice is not true . If you cast ServerHttpResponse to ServletServerHttpResponse , you can do both of them. So simply implement a ResponseBodyAdvice :

    @ControllerAdvice
    public class CacheControlBodyAdvice implements ResponseBodyAdvice {
    
        @Override
        public boolean supports(MethodParameter returnType, Class converterType) {
            return true;
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    
            if(response instanceof ServletServerHttpResponse) {
                    ServletServerHttpResponse res= (ServletServerHttpResponse)(response);
                    if(res.getServletResponse().getStatus() == 400){
                        res.getServletResponse().setHeader("Cache-Control", "XXXXX");
                    }           
            }
            return body;
        }
    }
    

    One more thing need to pay attention is that if your controller method throws an exception before complete normally , depending on how to handle the exceptions , the ResponseBodyAdvice may not be trigger. So , I suggest to implement the same logic in the GlobalErrorHandler for safety guard :

    @ControllerAdvice
    public class GlobalErrorHandler{
    
       @ExceptionHandler(value = Exception.class)
       public void handle(HttpServletRequest request, HttpServletResponse response) {
    
            if(response.getStatus() == 400){
               response.setHeader("Cache-Control", "XXXXX");
            }       
        }
    }