61
votes

I have implemented a REST server using Spring Boot 1.0.2. I'm having trouble preventing Spring from setting HTTP headers that disable HTTP caching.

My controller is as following:

@Controller
public class MyRestController {
    @RequestMapping(value = "/someUrl", method = RequestMethod.GET)
    public @ResponseBody ResponseEntity<String> myMethod(
            HttpServletResponse httpResponse) throws SQLException {
        return new ResponseEntity<String>("{}", HttpStatus.OK);
    }
}

All HTTP responses contain the following headers:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Expires: 0
Pragma: no-cache

I've tried the following to remove or change those headers:

  1. Call setCacheSeconds(-1) in the controller.
  2. Call httpResponse.setHeader("Cache-Control", "max-age=123") in the controller.
  3. Define @Bean that returns WebContentInterceptor for which I've called setCacheSeconds(-1).
  4. Set property spring.resources.cache-period to -1 or a positive value in application.properties.

None of the above have had any effect. How do I disable or change these headers for all or individual requests in Spring Boot?

9
I don't think Spring Boot does that (not in any of the samples I tried anyway). Maybe you can share a minimal project that has these headers in the responses?Dave Syer
You are right. The culprit turned out to be Spring Security.Samuli Pahaoja

9 Answers

63
votes

Turns out the no-cache HTTP headers are set by Spring Security. This is discussed in http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#headers.

The following disables the HTTP response header Pragma: no-cache, but doesn't otherwise solve the problem:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Prevent the HTTP response header of "Pragma: no-cache".
        http.headers().cacheControl().disable();
    }
}

I ended up disabling Spring Security completely for public static resources as following (in the same class as above):

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/static/public/**");
}

This requires configuring two resource handlers to get cache control headers right:

@Configuration
public class MvcConfigurer extends WebMvcConfigurerAdapter
        implements EmbeddedServletContainerCustomizer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // Resources without Spring Security. No cache control response headers.
        registry.addResourceHandler("/static/public/**")
            .addResourceLocations("classpath:/static/public/");

        // Resources controlled by Spring Security, which
        // adds "Cache-Control: must-revalidate".
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(3600*24);
    }
}

See also Serving static web resources in Spring Boot & Spring Security application.

16
votes

There are a lot of ways in spring boot for http caching. Using spring boot 2.1.1 and additionally spring security 5.1.1.

1. For resources using resourcehandler in code:

You can add customized extensions of resources this way.

registry.addResourceHandler

Is for adding the uri path where to get the resource

.addResourceLocations

Is for setting the location in the filesystem where the resources are located( given is a relative with classpath but absolute path with file::// is also possible.)

.setCacheControl

Is for setting the cache headers (self explanatory.)

Resourcechain and resolver are optional (in this case exactly as the default values.)

@Configuration
public class CustomWebMVCConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/*.js", "/*.css", "/*.ttf", "/*.woff", "/*.woff2", "/*.eot",
            "/*.svg")
            .addResourceLocations("classpath:/static/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)
                    .cachePrivate()
                    .mustRevalidate())
            .resourceChain(true)
            .addResolver(new PathResourceResolver());
    }
}

2. For resources using application properties config file

Same as above, minus the specific patterns, but now as config. This configuration is applied to all resources in the static-locations listed.

spring.resources.cache.cachecontrol.cache-private=true
spring.resources.cache.cachecontrol.must-revalidate=true
spring.resources.cache.cachecontrol.max-age=31536000
spring.resources.static-locations=classpath:/static/

3. At controller level

Response here is the HttpServletResponse injected in the controller method as parameter.

no-cache, must-revalidate, private

getHeaderValue will output the cache options as string. e.g.

response.setHeader(HttpHeaders.CACHE_CONTROL,
            CacheControl.noCache()
                    .cachePrivate()
                    .mustRevalidate()
                    .getHeaderValue());
8
votes

I have found this Spring extension: https://github.com/foo4u/spring-mvc-cache-control.

You just have to do three steps.

Step 1 (pom.xml):

<dependency>
    <groupId>net.rossillo.mvc.cache</groupId>
    <artifactId>spring-mvc-cache-control</artifactId>
    <version>1.1.1-RELEASE</version>
    <scope>compile</scope>
</dependency>

Step 2 (WebMvcConfiguration.java):

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CacheControlHandlerInterceptor());
    }
}

Step 3 (Controller):

@Controller
public class MyRestController {

    @CacheControl(maxAge=31556926)
    @RequestMapping(value = "/someUrl", method = RequestMethod.GET)
    public @ResponseBody ResponseEntity<String> myMethod(
            HttpServletResponse httpResponse) throws SQLException {
        return new ResponseEntity<String>("{}", HttpStatus.OK);
    }
}
5
votes

Overriding of default caching behavior for a particular method can be done in the below way:

@Controller
public class MyRestController {
    @RequestMapping(value = "/someUrl", method = RequestMethod.GET)
    public @ResponseBody ResponseEntity<String> myMethod(
            HttpServletResponse httpResponse) throws SQLException {
        return new ResponseEntity.ok().cacheControl(CacheControl.maxAge(100, TimeUnit.SECONDS)).body(T)
    }
}
2
votes

The CacheControl class is a fluent builder, which makes it easy for us to create different types of caching:

@GetMapping("/users/{name}")
public ResponseEntity<UserDto> getUser(@PathVariable String name) { 
    return ResponseEntity.ok()
      .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
      .body(new UserDto(name));
}

Let's hit this endpoint in our test, and assert that we have changed the headers:

given()
  .when()
  .get(getBaseUrl() + "/users/Michael")
  .then()
  .header("Cache-Control", "max-age=60");
0
votes

I run into similar problem. I wanted to get just some of dynamic resources (images) cached in the browser. If image changes (not very often) I change the part of uri... This is my sollution

    http.headers().cacheControl().disable();
    http.headers().addHeaderWriter(new HeaderWriter() {

        CacheControlHeadersWriter originalWriter = new CacheControlHeadersWriter();

        @Override
        public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
            Collection<String> headerNames = response.getHeaderNames();
            String requestUri = request.getRequestURI();
            if(!requestUri.startsWith("/web/eventImage")) {
                originalWriter.writeHeaders(request, response);
            } else {
               //write header here or do nothing if it was set in the code
            }       
        }
    });
0
votes
@Configuration
@EnableAutoConfiguration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/resources/")
                .setCachePeriod(31556926);

    }
}
0
votes

If you don't care to have your static resources authenticated, you could do this:

import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toStaticResources;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
    @Override
    public void configure(WebSecurity webSecurity) throws Exception {
        webSecurity
                .ignoring()
                .requestMatchers(toStaticResources().atCommonLocations());
    }
...
}

and in your application.properties:

spring.resources.cache.cachecontrol.max-age=43200

See ResourceProperties.java for more properties that can be set.

0
votes

I used below lines in my controller.

ResponseEntity.ok().cacheControl(CacheControl.maxAge(secondWeWantTobeCached, TimeUnit.SECONDS)).body(objToReturnInResponse);

Please note that Response will have header Cache-Control with value secondWeWantTobeCached. However if we are typing url in addressbar and pressing enter, Request will always be sent from Chrome to server. However if we hit url from some link, browser will not send a new request and it will be taken from cache.