Skip to content
/

Make your browser cache the output of an HttpHandler

Recently I worked on an HttpHandler implementation that is serving images from a backend system. Although everything seemed to work as expected it was discovered images were requested by the browser on every page refresh instead of caching them locally. Together with my colleague Bert-Jan I investigated and solved the problem which will be explained in this post.

The problem

Let’s start with the original (simplified) code. This code gets the image from the backend system (in this case Content Management Server 2002) and serves it to the browser, or in case the resource is not available it will return “404 Not Found“.

public class ResourceHandler : IHttpHandler
{
  public void ProcessRequest(HttpContext context)
  {
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.Cache.SetMaxAge(new TimeSpan(1, 0, 0));

    string imagePath = "some path";

    Resource resource = CmsHttpContext.Current.RootResourceGallery
                                    .GetByRelativePath(imagePath) as Resource;

    if (resource == null)
    {
      // Resource not found
      context.Response.StatusCode = 404;
      return;
    }

    using (Stream stream = resource.OpenReadStream())
    {
      byte[] buffer = new byte[32];
      while (stream.Read(buffer, 0, 32) > 0)
      {
        context.Response.BinaryWrite(buffer);
      }
    }
  }

  public bool IsReusable
  {
    get { return true; }
  }
}

Every time the browser requested a resource it responded with the following headers and included the full image.

HTTP/1.1 200 OK
Cache-Control: public
Content-Length: 3488
Content-Type: image/gif
Server: Microsoft-IIS/6.0
X-AspNet-Version: 2.0.50727
COMMERCE-SERVER-SOFTWARE: Microsoft Commerce Server, Enterprise Edition
X-Powered-By: ASP.NET
Date: Fri, 11 Mar 2011 10:51:08 GMT

GIF89a... (the raw image)

The CAUSE

For some reason the local browser cache was omitted. We fired up Fiddler and started comparing the headers to an other source where an image was getting cached locally.

On the first request we discovered an additional header “Last Modified”:

Last-Modified: Tue, 05 Jun 2007 15:19:48 GMT

The second response we got to the same image resulted not in a “200 OK” but a “304 Not Modified” message.

HTTP/1.1 304 Not Modified
Connection: close
Date: Fri, 11 Mar 2011 12:21:55 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-AspNet-Version: 2.0.50727
Cache-Control: public
Last-Modified: Tue, 05 Jun 2007 15:19:48 GMT

The Solution

So the first thing missing was the “Last Modified” entry in our first response. We added code to include this property.

context.Response.Cache.SetLastModified(resource.LastModifiedDate);

By adding the “Last Modified” date the browser added a new entry to the second request of the image:

If-Modified-Since: Tue, 05 Jun 2007 15:19:48 GMT

But the response was still the same “200 OK” with the complete image. As it turned out you need to handle the “If-Modified-Since” yourself. We added the following code to handle this.

string rawIfModifiedSince = context.Request.Headers.Get("If-Modified-Since");
if (string.IsNullOrEmpty(rawIfModifiedSince))
{
  // Set Last Modified time
  context.Response.Cache.SetLastModified(res.LastModifiedDate);
}
else
{
  DateTime ifModifiedSince = DateTime.Parse(rawIfModifiedSince);

  if (resource.LastModifiedDate == ifModifiedSince)
  {
    // The requested file has not changed
    context.Response.StatusCode = 304;
    return;
  }
}

After testing this again the image was still transmitted every time it was requested. A quick debug of the date compare revealed that the HTTP request date time does not contain milliseconds. The following fix was applied.

if (resource.LastModifiedDate.AddMilliseconds(
                   -resource.LastModifiedDate.Millisecond) == ifModifiedSince)

Now every following request returned a “304 Not Modified” and saves us a lot of traffic and loading time!

Summary

To conclude this post I give you the complete code:

using System;

public class ResourceHandler : IHttpHandler
{
  public void ProcessRequest(HttpContext context)
  {
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.Cache.SetMaxAge(new TimeSpan(1, 0, 0));

    string imageName = "some path"

    Resource resource = CmsHttpContext.Current.RootResourceGallery
                                    .GetByRelativePath(imageName) as Resource;

    if (resource == null)
    {
      // Resource not found
      context.Response.StatusCode = 404;
      return;
    }

    string rawIfModifiedSince = context.Request.Headers
                                             .Get("If-Modified-Since");
    if (string.IsNullOrEmpty(rawIfModifiedSince))
    {
      // Set Last Modified time
      context.Response.Cache.SetLastModified(resource.LastModifiedDate);
    }
    else
    {
      DateTime ifModifiedSince = DateTime.Parse(rawIfModifiedSince);

      // HTTP does not provide milliseconds, so remove it from the comparison
      if (resource.LastModifiedDate.AddMilliseconds(
                  -resource.LastModifiedDate.Millisecond) == ifModifiedSince)
      {
          // The requested file has not changed
          context.Response.StatusCode = 304;
          return;
      }
    }

    using (Stream stream = resource.OpenReadStream())
    {
      byte[] buffer = new byte[32];
      while (stream.Read(buffer, 0, 32) > 0)
      {
          context.Response.BinaryWrite(buffer);
      }
    }
  }

  public bool IsReusable
  {
    get { return true; }
  }
}

3 Comments

Leave a comment
  1. Stefan Emanuelsson / Jul 11 2012

    Thanks, helped a lot. As a slightly paranoid coder i would prefer using

    if (resource.LastModifiedDate.AddMilliseconds(
    -resource.LastModifiedDate.Millisecond) > ifModifiedSince)

    Instead of comparing for exact the same DateTime’s, although the browser should just echo back the server’s last last-modified this feels a little bit less bug-prone (or should i say “future safe” :-) )

  2. Stefan Emanuelsson / Jul 11 2012

    Sorry, should be >= in the previous comment…

  3. Max / Mar 6 2013

    The article was a big help for me to implement Microsoft’s Ajax Utilities as an ASPX module (using a filter that shrinks Javascript on detection in certain file types). Only that in FileInfo.LastWriteTime (which I used to decide on caching) I had to remove the ticks, not the milliseconds:

    fileInfo.LastWriteTime.AddTicks(-fileInfo.LastWriteTime.Ticks % TimeSpan.TicksPerSecond)

Leave a comment


*