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 the browser caching them locally.

Together with Bert-Jan, I investigated and solved the problem which will be explained in this article.

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 another 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 article, 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; }
  }
}