I have a controller annotated @RestController in Spring Boot (v2.4). This should return a HTTP header "Cache-Control" with a quite static configuration by default for all endpoints it handles.
Until now I achieved this with a @ModelAttribute annotated method inside the controller class that simply sets the headers like this:
@ModelAttribute
public void setResponseHeader(HttpServletResponse response) {
response.setHeader(HttpHeaders.CACHE_CONTROL,
CacheControl.maxAge(1, TimeUnit.DAYS).cachePrivate().getHeaderValue());
}
This works fine, but I found out that it collides a bit with my exception handler - I configured a global one by putting @RestControllerAdvice onto another class and then having multiple methods annotated with @ExceptionHandler for the various possible exceptions inside.
The exception handlers most of the time set a specific HTTP status of 400 and above and of course I don't want that to be cached.
Even if I make the HttpServletResponse available inside the exception handling methods, the HTTP headers are already set in there by my setResponseHeader(HttpServletResponse) method and the interface HttpServletResponse doesn't allow me to remove them again.
So is there any strategy to have the default caching headers on one side, but exceptions for the Exception handlers on the other side?
I really would appreciate if I don't have to set the caching headers individually inside my controller methods per endpoint.
Additional challenge is, that I need to stream big data to the client, so I really would like to avoid server-side caching because I expect lots of requests and want to prevent that the server fills its heap with the contents that should be delivered to the clients. Unfortunately I cannot use the plain resource handler mechanism since complex queries regarding permissions (not just OAuth2, but licensing) are required that I don't want to put into a filter.
CodePudding user response:
This servlet filter sets the Cache-Control header, except when the response status is a 4xx error:
public class CacheControlFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
var responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);
if (!HttpStatus.valueOf(responseWrapper.getStatus()).is4xxClientError()) {
responseWrapper.setHeader(
HttpHeaders.CACHE_CONTROL,
CacheControl.maxAge(1, TimeUnit.DAYS).cachePrivate().getHeaderValue());
}
responseWrapper.copyBodyToResponse();
}
}
You would also need to delete the other code that sets the header, because the header should be set in only one place.
CodePudding user response:
After several tries, I found this solution for my special situation:
- I stick to the
@ModelAttributeto fill in default caching header. - In the
@ExceptionHandlerannotated methods I overwrite the caching header with a "no-cache" value.
package test;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletResponse;
@RestControllerAdvice
public class ExHandlers {
@ExceptionHandler({NullPointerException.class, IllegalStateException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String nullPointer(RuntimeException e, HttpServletResponse response) {
response.setHeader(HttpHeaders.CACHE_CONTROL, CacheControl.noCache().getHeaderValue())
return e.getMessage();
}
}
Important note: Don't use org.springframework.http.ResponseEntity as this will add a second Cache-Control header to the response, while HttpServletResponse.setHeader(...) replaces the default one.
I checked the implementations and it is not possible to actively remove the header once it is set.
Downside of my solution: when the body has started to be streamed and an exception occurs, I cannot "revert" the headers - probably they have gone over the wire yet. But with the wish for no server-side caching I think that's the only way to go.
