0

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.

javapapo
  • 1,342
  • 14
  • 26

3 Answers3

2

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");
        }       
    }
}
Ken Chan
  • 84,777
  • 26
  • 143
  • 172
  • This is exactly my problem! The second part. So I have a filter, that anyway adds some default Cache-Control headers per request. Which is fine and works. Now when if any of these controllers there is an exception, the ControllerAdvice wil do the exception handling and will return a custom error msg. What I see is that the Cache-Control header is added anyway to to response by the filter,and when I try to do any extra logic - e.g most probably change the value of Header on the Error Handler like you show above, I end up with the same header and conflicting values!!!! – javapapo Feb 03 '19 at 12:28
  • I have updated my question in order to `indicate` this weird - behavior as discussed above – javapapo Feb 03 '19 at 12:40
  • 1
    @javapapo It turns out that `response.setHeader()` is different from the `response.addHeader()` . The latter one will add the header value on top of the existing value of the same header which allow the same header to have multiple values , so it may cause the duplicated header issues that you mentioned. I revised the codes in the `GlobalErrorHandler` to use `response.setHeader()` in order to prevent such issue. Also , what I suggest is to use `CacheControlBodyAdvice` but not your `CacheControlFilter` ... – Ken Chan Feb 03 '19 at 13:17
  • Correct - many thanks. I think I will just do that, replace the Filter with the Advice, it seems like the only way to actually have full control and not get crazy! – javapapo Feb 03 '19 at 16:17
0

I supposed that you are using spring-mvc (As you mentioned in your tags); If so you can bind to HttpServletResponse to add your headers. You can do it in your method handler like so:

@RestController
class HelloWordController{

    @GetMapping("/hello")
    public String test(HttpServletResponse response){
        response.addHeader("test", "123");
        return "hola";
    }
}

Another solution (fashion) would be to return a ResponseEntity instead :

@RestController
class HelloWorkController{

    @GetMapping("/hello")
    public ResponseEntity<String> test(HttpServletResponse response){
        return ResponseEntity.status(HttpStatus.OK)
                .header("test", "4567")
                .body("hello world");
    }
}
Abdelghani Roussi
  • 2,707
  • 2
  • 21
  • 39
  • Maybe misleading phrasing from my side - This works! But it seems that the filter adds the response headers before we do any actual business logic. Lets say that my system calls a downstream service and it gets a 404 response it wants to return as a well a 404, so only when I have done any processing, and I am ready to return to the client - my response I want to set the header and not before. At least this is what I see is happening. Filter -> Filter -> Controller/Service – javapapo Feb 03 '19 at 12:35
-1

There are a dozen of ways of changing a HttpServletResponse before return to client in Spring and injecting the response into the handler method or leveraging ControllerAdvice are valid solutions. However, I don't understand the underlying premise of your question that filters can't do the job:

I have already 2 OncePerRequestFilter registered, which work fine - but obviously I can not do logic - once the processing is complete.

As far as modifying HttpServletResponse is concerned, Filters work totally fine for me and are at least as suitable as any other tool for that job:

@Bean
public FilterRegistrationBean createFilter() {
    Filter filter = new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            super.doFilter(request, response, filterChain);
            response.setHeader("Cache-Control", "xxx");
        }
    };
    return new FilterRegistrationBean(filter);
}
Fritz Duchardt
  • 11,026
  • 4
  • 41
  • 60
  • Maybe misleading phrasing from my side - This works! But it seems that the filter adds the response headers before we do any actual business logic. Lets say that my system calls a downstream service and it gets a 404 response it wants to return as a well a 404, so only when I have done any processing, and I am ready to return to the client - my response I want to set the header and not before. At least this is what I see is happening. Filter -> Filter -> Controller/Service ... – javapapo Feb 03 '19 at 12:23
  • 1
    In a Filter implementation wIthin the doFilterInternal method the code before the call to doFilter is executed before the Controller, the code after the call to doFilter is executed after the Controller. – Fritz Duchardt Feb 03 '19 at 15:58
  • After doFilter is too late, the response may have been flushed/committed, which means modifications to the response here are ignored. – Adrian Baker Feb 12 '21 at 20:15