Image from HttpHandler won't cache in browser
Asked Answered
I

4

21

I'm serving up an image from a database using an IHttpHandler. The relevant code is here:

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "image/jpeg";
    int imageID;
    if (int.TryParse(context.Request.QueryString["id"], out imageID))
    {
        var photo = new CoasterPhoto(imageID);
        if (photo.CoasterPhotoID == 0)
            context.Response.StatusCode = 404;
        else
        {
            byte[] imageData = GetImageData(photo);
            context.Response.OutputStream.Write(imageData, 0, imageData.Length);
            context.Response.Cache.SetCacheability(HttpCacheability.Public);
            context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(5));
            context.Response.Cache.SetLastModified(photo.SubmitDate);
        }
    }
    else
        context.Response.StatusCode = 404;
}

The problem is that the browser won't cache the image, presumably because I'm not indicating the right thing in the response headers. The part calling methods on the HttpCachePolicy property is what I thought would force the browser to hold on to the image, but it doesn't. I think the "right" thing is for the handler to return a 304 status code without an image, right? How do I achieve that using IHttpHandler?

EDIT:

Per the best answer, I got this code running and it completely solves the problem. Yes, it needs some refactoring, but it generally demonstrates what I was after. The relevant parts:

if (!String.IsNullOrEmpty(context.Request.Headers["If-Modified-Since"]))
{
    CultureInfo provider = CultureInfo.InvariantCulture;
    var lastMod = DateTime.ParseExact(context.Request.Headers["If-Modified-Since"], "r", provider).ToLocalTime();
    if (lastMod == photo.SubmitDate)
    {
        context.Response.StatusCode = 304;
        context.Response.StatusDescription = "Not Modified";
        return;
    }
}
byte[] imageData = GetImageData(photo);
context.Response.OutputStream.Write(imageData, 0, imageData.Length);
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetLastModified(photo.SubmitDate);
Inescutcheon answered 15/6, 2009 at 0:41 Comment(7)
It looks like the browser should cache it for 5 minutes. Is it caching it for even that long? Is that what you intend?Nearby
The browser does not cache at all. This code executes every time the request is made.Inescutcheon
Completely unrelated, but do you have any performance implications when using this approach for serving images? Especially when you serve several images in a webpage with your IHttpHandler?Demesne
None that I've encountered. Keep in mind that I use the async version of the httphandler though, just in case there's some run on threads.Inescutcheon
I know this thread old, but would you mind explaining your choice to use OutputStream.Write over WriteFile? I'm trying to decide which I wanna use and why. Thanks.Eelpout
Because I'm writing to a stream and not a file?Inescutcheon
be careful with the (lastMod == photo.SubmitDate) line, as I saw, the lastMod parameter can only be parsed to seconds, not milliseconds - in my case it failed on comparing with File.CreatonDateTime(), because of it :)Vinaigrette
M
24

AFAIK, you are responsible for sending 304 Not Modified, meaning I am not aware of anything in the .Net framework that does it for you in this use case of you sending "dynamic" image data. What you will have to do (in pseudo code):

  • Check for the If-Modified-Since header in the request and parse out the date (if it exists).
  • Compare it to the last modification date of your original image (dynamically generated) image. Tracking this is probably the most complex part of the solution to this problem. In your current situation, you are re-creating the image on every request; you don't want to do that unless you absolutely have to.
  • If the date of the file the browser has is newer or equal to what you have for the image, send a 304 Not Modified.
  • Otherwise, continue with your current implementation

A simple way to track last modified times on your end is to cache newly generated images on the file system and keep an in-memory dictionary around that maps the image ID to a struct containing the file name on disk and the last modification date. Use Response.WriteFile to send the data from disk. Of course, every time you restart your worker process, the dictionary would be empty, but you're getting at least some caching benefit without having to deal with persisting caching information somewhere.

You can support this approach by separating the concerns of "Image Generation" and "Sending Images over HTTP" into different classes. Right now you're doing two very different things in the same place.

I know this may sound a little complex, but it's worth it. I just recently implemented this approach and the savings in processing time and bandwidth usage were incredible.

Monochord answered 15/6, 2009 at 1:17 Comment(0)
H
7

If you have source file on disk you can use this code:

context.Response.AddFileDependency(pathImageSource);
context.Response.Cache.SetETagFromFileDependencies();
context.Response.Cache.SetLastModifiedFromFileDependencies();
context.Response.Cache.SetCacheability(HttpCacheability.Public);

Also, make sure that you test using IIS, not from Visual Studio. ASP.NET Development Server (aka Cassini) always sets Cache-Control to private.

See also: Caching Tutorial for Web Authors and Webmasters

Heptarchy answered 15/6, 2009 at 4:52 Comment(0)
P
6

This is how it's done in Roadkill's (a .NET wiki) file handler:

FileInfo info = new FileInfo(fullPath);
TimeSpan expires = TimeSpan.FromDays(28);
context.Response.Cache.SetLastModifiedFromFileDependencies();
context.Response.Cache.SetETagFromFileDependencies();
context.Response.Cache.SetCacheability(HttpCacheability.Public);

int status = 200;
if (context.Request.Headers["If-Modified-Since"] != null)
{
    status = 304;
    DateTime modifiedSinceDate = DateTime.UtcNow;
    if (DateTime.TryParse(context.Request.Headers["If-Modified-Since"], out modifiedSinceDate))
    {
        modifiedSinceDate = modifiedSinceDate.ToUniversalTime();
        DateTime fileDate = info.LastWriteTimeUtc;
        DateTime lastWriteTime = new DateTime(fileDate.Year, fileDate.Month, fileDate.Day, fileDate.Hour, fileDate.Minute, fileDate.Second, 0, DateTimeKind.Utc);
        if (lastWriteTime != modifiedSinceDate)
            status = 200;
    }
}

context.Response.StatusCode = status;

Thomas's answer about IIS not supplying the status code is the key, without it you just get 200s back each time.

The browser will simply send you a date and time for when it thinks the file was last modified (no no header at all), so if it differs you just return a 200. You do need to normalize your file's date to remove milliseconds and ensure it's a UTC date.

I've gone for defaulting to 304s if there's a valid modified-since, but that can be tweaked if needed.

Pteridophyte answered 22/3, 2013 at 22:9 Comment(0)
S
0

Do you have any response buffering happening? If so you might want to set the headers before you write to the output stream. i.e. try moving the Response.OutputStream.Write() line down to below the Cache setting lines.

Surrender answered 15/6, 2009 at 1:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.