4

We are serving javascript resources (and others) via wro in our webapp. On the PROD environment, the browser gets (for example) the app.js angular webapp's content with an 'expires' headers one year in the future.

Meaning that for subsequent requests the browser takes it from cache without a request to the server. If we deploy a new version of the webapp, the browser does not get the new version, as it takes it from the local cache.

The goal is to configure wro or/and spring so that the headers will be correctly set to have the browser perform the request each time, and the server return a 304 not modified. So we would have the clients automatically "updated" uppon new deployment. Did someone already achieve this?

We use Spring's Java Configuration:

@Configuration
public class Wro4jConfiguration {   

@Value("${app.webapp.web.minimize}")
private String minimize;

@Value("${app.webapp.web.disableCache}")
private String disableCache;

@Autowired
private Environment env;

@Bean(name = "wroFilter")
public WroFilter wroFilter() {
    ConfigurableWroFilter filter = new ConfigurableWroFilter();
    filter.setWroManagerFactory(new Wro4jManagerFactory());
    filter.setWroConfigurationFactory(createProperties());
    return filter;
}

private PropertyWroConfigurationFactory createProperties() {
    Properties props = new Properties();
    props.setProperty("jmxEnabled", "false");
    props.setProperty("debug", String.valueOf(!env.acceptsProfiles(EnvConstants.PROD)));
    props.setProperty("gzipResources", "false");
    props.setProperty("ignoreMissingResources", "true");
    props.setProperty("minimizeEnabled", minimize);
    props.setProperty("resourceWatcherUpdatePeriod", "0");
    props.setProperty("modelUpdatePeriod", "0");
    props.setProperty("cacheGzippedContent", "false");
    // let's see if server-side cache is disabled (DEV only)
    if (Boolean.valueOf(disableCache)) {
        props.setProperty("resourceWatcherUpdatePeriod", "1");
        props.setProperty("modelUpdatePeriod", "5");
    }
    return new PropertyWroConfigurationFactory(props);
}
}
Gaël Oberson
  • 603
  • 1
  • 5
  • 14

2 Answers2

3

By default, WroFilter set the following headers: ETag (md5 checksum of the resource), Cache-Control (public, max-age=315360000), Expires (1 year since resource creation).

There are plenty of details about the significance of those headers. The short explanation is this:

When the server reads the ETag from the client request, the server can determine whether to send the file (HTTP 200) or tell the client to just use their local copy (HTTP 304). An ETag is basically just a checksum for a file that semantically changes when the content of the file changes. If only ETag is sent, the client will always have to make a request.

The Expires and Cache-Control headers are very similar and are used by the client (and proxies/caches) to determine whether or not it even needs to make a request to the server at all.

So really what you want to do is use BOTH headers - set the Expires header to a reasonable value based on how often the content changes. Then configure ETags to be sent so that when clients DO send a request to the server, it can more easily determine whether or not to send the file back.

If you want the client always to check for the latest resource version, you should not send the expires & cache-control headers.

Alternatively, there is a more aggressive caching technique: encode the checksum of the resource into its path. As result, every time a resource is changed, the path to that resource is changed. This approach guarantee that the client would always request the most recent version. For this approach, in theory the resources should never expire, since the checksum change every time a resource is changed.

Alex Objelean
  • 3,893
  • 2
  • 27
  • 36
2

Based on Alex's information and documentation reference, I ended up overriding WroFilter.setResponseHeaders to put appropriate expire values. This is working fine. Wro already takes care of setting ETag, Date and others, so I only overwrite the expiration delay and date.

@Configuration
public class Wro4jConfiguration {

    @Value("${app.webapp.web.browserCache.maxAgeInHours}")
    private String maxAgeInHours;

    @Bean(name = "wroFilter")
    public WroFilter wroFilter() {
        ConfigurableWroFilter filter = createFilter();

        filter.setWroManagerFactory(new Wro4jManagerFactory());
        filter.setWroConfigurationFactory(createProperties());

        return filter;
    }

    private ConfigurableWroFilter createFilter() {
        return new ConfigurableWroFilter() {

            private final int BROWSER_CACHE_HOURS = Integer.parseInt(maxAgeInHours);
            private final int BROWSER_CACHE_SECONDS = BROWSER_CACHE_HOURS * 60 * 60;

            @Override
            protected void setResponseHeaders(final HttpServletResponse response){
                super.setResponseHeaders(response);

                if (!getConfiguration().isDebug()) {
                    ZonedDateTime cacheExpires = ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("GMT")).plusHours(BROWSER_CACHE_HOURS);
                    String cacheExpiresStr = cacheExpires.format(DateTimeFormatter.RFC_1123_DATE_TIME);

                    response.setHeader(HttpHeader.EXPIRES.toString(), cacheExpiresStr);
                    response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "public,  max-age=" + BROWSER_CACHE_SECONDS);
                }
            }
        };
    }

    // Other config methods

}
Gaël Oberson
  • 603
  • 1
  • 5
  • 14