I recently deployed a release of our product which caused a majority of our users to clear their browsers cache in order to access our sites. Of course, this is quite embarrassing and I was asked to identify the problem. Since day one we've always had the standard solution of deploying a ServletFilter which sets the standard Cache-Control headers so it was confusing why all of a sudden a large percent of users were reporting this issue.
The good news is that I believe I've solved my problem and since I've never seen any reference to it I figured it would write it down.
Disclaimer: You'll have to forgive me but we're running Tomcat 7.0.22 (yeah, I know) so this might have changed in more recent versions.
So the first thing that I noticed is that in the tomcat access logs there are HTTP 304 (File Not Modified) being returned for my nocache.js files. After many hours of Googling and debugging, I found that the DefaultServlet. checkIfModifiedSince() compares the files timestamp with the "If-Not-Modified" header to determine if that file has changed. We're deploying our app as a WAR file which means that files timestamp gets set to the time that is stored in the WAR. Now to further complicate this issue GWT seems to be caching this file someone (not the GWT working directory) so in our build it was getting set to the time when we created the branch and jenkins plan.
So to summarize, my problem was the timestamp of this file was being set to the time of the branch creation (which was days before deployment). So anyone who used the application between the time we branched and them time we deployed would cache a copy of the nocache.js file with the current timestamp. When the WAR was finally deployed, the timestamp was updated to branch time and new request would start getting 304s because they were passing in "If-Modified-Since" headers that were greater than the time of the branch.
So how do you fix this problem? A simple unix command:
find -name '*.nocache.js" -exec touch {} \;
Now, I also went a step further and wanted to try to fix the programmatically. The only workaround that I could find was to build an HttpServletRequest interceptor that prevents that header from being returned.
public class GWTCacheControlFilter implements Filter { private static final long SECONDS = 1000; private static final long MINUTES = 60 * SECONDS; private static final long HOURS = 60 * MINUTES; private static final long DAYS = 24 * HOURS; public void destroy() { } public void init(FilterConfig config) throws ServletException { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String requestURI = httpRequest.getRequestURI(); if (requestURI.contains(".nocache.")) { try { request = createProxy(httpRequest); } catch (Throwable t) { logger.error("Failed to creative interceptor",t); } Date now = new Date(); HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setDateHeader("Date", now.getTime()); httpResponse.setDateHeader("Last-Modified", now.getTime() ); httpResponse.setDateHeader("Expires", now.getTime() - 30 * DAYS ); httpResponse.setHeader("Pragma", "no-cache"); httpResponse.setHeader("Cache-control", "no-cache, no-store, must-revalidate, max-age=0"); } filterChain.doFilter(request, response); } private static final HttpServletRequest createProxy(final HttpServletRequest source) { return (HttpServletRequest) Proxy.newProxyInstance( GWTCacheControlFilter.class.getClassLoader() , new Class[] { HttpServletRequest.class } , new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName(); if ( name.equals("getDateHeader") && args.length == 1 && args[0].equals("If-Modified-Since") ) { return 0l; } if ( name.equals("getHeader") && args.length == 1 && args[0].equals("If-Modified-Since") ) { return null; } if ( name.equals("getIntHeader") && args.length == 1 && args[0].equals("If-Modified-Since") ) { return 0; } return method.invoke(source, args); } }); } }
3 comments:
Quick follow up...
I deployed this fix into production and it did solve the problem but the downside of the programmatic/interceptor solution is that it truly prevents that file from being cached. This does result in a slightly delay accessing the site due to that extra download. So you might want to consider the "touch" solution.
Kevin, many thanks for this, Bud. You saved me from several days of additional grief trying to figure this out for myself. Kudos for taking the time to post this!
One thing that I forgot to mention is that this also effects all static content. So if you replace and image file you'll have this same problem. Hence the best solution really is just to update ALL timestamp on deployment.
Post a Comment