Hacking the browser cache with JQuery and ASP.NET MVC

Although JQuery provides a very good support for caching responses from AJAX calls in the browser, it is always good to know how you can use http as protocol for making an effective use of it.

The first thing you need to do on the server side is to supports HTTP GETs, and identify your resources with different URLs for retrieving the data (The resource in this case could be just a MVC action). If you use the same URL for retrieving different resource representations, you are doing it wrong. An HTTP POST will not work either as the response can not cached. Many devs typically use HTTP POSTs for two reason, they want to make explicit the data can not be cached or they use it as workaround for avoiding JSON hijacking attacks, which can be avoided anyways in HTTP GETs by returning a JSON array.

The ajax method in the JQuery global object provides a few options for supporting caching and conditional gets,

$.ajax({
    ifModified: [true|false],
    cache: [true|false],
});

The “ifModified” flag specifies whether we want to support conditional gets in the ajax calls. JQuery will automatically handle everything for us by picking the last received “Last-Modified” header from the server, and sending that as “If-Modified-Since” in all the subsequent requests. This requires that our MVC controller implements conditional gets. A conditional get in the context of http caching is used to revalidate an expired entry in the cache. If JQuery determines an entry is expired, it will be first try to revalidate that entry using a conditional get against the server. If the response returns an status code 304 (Not modified), JQuery will reuse the entry in the cache. In that way, we can save some of the bandwidth required to download the complete payload associated to that entry from the server.

The “cache” option basically overrides all the caching settings sent by the server as http headers. By setting this flag to false, JQuery will add an auto generated timestamp in the end of the URL to make it different from any previous used URL, so the browser will not know how to cache the responses.

Let’s analyze a couple scenarios.

The server sets a No-Cache header on the response

The server is the king. If the server explicitly states the response can not be cached, JQuery will honor that. The “cache” option on the ajax call will be completely ignored.

$('#nocache').click(function () {
    $.ajax({
        url: '/Home/NoCache',
        ifModified: false,
        cache: true,
        success: function (data, status, xhr) {
            $('#content').html(data.count);
        }
    });
});
public ActionResult NoCache()
{
   Response.Cache.SetCacheability(HttpCacheability.NoCache);
   return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
}

The server sets an Expiration header on the response

Again, the server is always is the one in condition for setting an expiration time for the data it returns. The entry will cached on the client side using that expiration setting.

$('#expires').click(function () {
    $.ajax({
        url: '/Home/Expires',
        ifModified: false,
        cache: true,
        success: function (data, status, xhr) {
            $('#content').html(data.count);
        }
    });
});
public ActionResult Expires()
{
    Response.Cache.SetExpires(DateTime.Now.AddSeconds(5));
    return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
}

The client never caches the data

The client side specifically states the data must be always fresh and the cache not be used. This means the “cache” option is set to false. No matter what the server specifies, JQuery will always generate an unique URL so that will be impossible to cache.

$('#expires_nocache').click(function () {
    $.ajax({
        url: '/Home/Expires',
        ifModified: false,
        cache: false,
        success: function (data, status, xhr) {
            $('#content').html(data.count);
        }
    });
});
public ActionResult Expires()
{
    Response.Cache.SetExpires(DateTime.Now.AddSeconds(5));
    return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
}

The client and server use conditional gets for validating the cached data.

The client puts a new entry in the cache, which will be validated after its expiration.  The server side must implement a conditional GET using either ETags or the last modified header.

$('#expires_conditional').click(function () {
    $.ajax({
        url: '/Home/ExpiresWithConditional',
        ifModified: true,
        cache: true,
        success: function (data, status, xhr) {
            $('#content').html(data.count);
        }
    });
});
public ActionResult ExpiresWithConditional()
{
    if (Request.Headers["If-Modified-Since"] != null && Count % 2 == 0)
    {
        return new HttpStatusCodeResult((int)HttpStatusCode.NotModified);
    }
    
    Response.Cache.SetExpires(DateTime.Now.AddSeconds(5));
    Response.Cache.SetLastModified(DateTime.Now);
 
    return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
}

The MVC action in the example above is only an example. In a real implementation, the server should be able to know whether the data has changed since the last time it was served or not.

Comments

No Comments