This might be one of those “Duh” moment posts that seems real obvious now, but this particular issue – trying to get OutputCache to work in an HttpHandler and having it not work because of an errand Response.End() - has caused me grief on a few occasions. Some time ago I posted a JavaScript Resource handler that serves server side ASP.NET resources to client side JavaScript. Bertrand commented on that post that I should be using OutputCache instead of writing values to the ASP.NET Element Cache as I had been in the code posted in the blog entry. I agree – OutputCache is probably the most efficient way to cache full requests. However, in the past I’ve had a number of issues with OutputCache not working in custom handlers and so I’ve been wary of using it in handler code falling back on using the ASP.NET Element cache instead. The Element Cache works well enough, but there’s additional processing required on each hit that OutputCache handles transparently especially if you need to do special encoding like GZip, so OutputCaching is much preferred. You can see the original code I used that uses the element cache in the post. It certainly would be nice to use OutputCache.
Output Caching
To review, OutputCaching is part of the ASP.NET Pipeline and is the same mechanism that you can use in ASP.NET Pages using the @OutputCache directive. Using OutputCache with a handler rather than a Page is a little less discoverable than the page directive, but it provides the same functionality for any code running in the pipeline. OutputChacing is implemented as a Module in the pipeline and depending on the type of request that is being processed (headers, length, whether it’s a POST operation etc.) the caching can get elevated to the IIS Kernel Cache (although information on when and how that actually occurs is very scarce). OutputCaching is probably the most efficient way to cache request because it is handled internally and doesn’t have to hit custom code if cached content is served. The content of a cached entry is fully held in the cache including headers, encoding and the content so there’s no requirement to re-encode or re-send headers etc. as you would when caching content on your own using the element cache as I did originally in the blog post code.
OutputCache is applied in its own step in the ASP.NET Event Pipeline via the ResolveRequestCache event which fires after request authorization (because you still want to authenticate/authorize cached requests) and before the request state is retrieved and a handler is loaded. For new entries the cache is then updated towards the end of the request in the UpdateRequestCache Event. You can see where in the pipeline these events fire on this old slide:
All this adds up to a very efficient mechanism of serving cached content because it fires very early in the pipeline processing and is handled internally by ASP.NET or (in IIS7) by IIS itself.
Writing to OutputCache is pretty straight forward and looks something like this:
HttpResponse Response = HttpContext.Current.Response;
// client cache
if (!HttpContext.Current.IsDebuggingEnabled)
{
Response.ExpiresAbsolute = DateTime.UtcNow.AddDays(30);
Response.Cache.SetLastModified(DateTime.UtcNow);
Response.AppendHeader("Accept-Ranges", "bytes");
Response.AppendHeader("Vary", "Accept-Encoding");
//Response.Cache.SetETag("\"" + javaScript.GetHashCode().ToString("x") + "\"");
}
// OutputCache settings
HttpCachePolicy cache = Response.Cache;
cache.VaryByParams["LocaleId"] = true;
cache.VaryByParams["ResoureType"] = true;
cache.VaryByParams["IncludeControls"] = true;
cache.VaryByParams["VarName"] = true;
cache.VaryByParams["IsGlobal"] = true;
cache.SetOmitVaryStar(true);
DateTime now = DateTime.Now;
cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(now + TimeSpan.FromDays(365.0));
cache.SetValidUntilExpires(true);
cache.SetLastModified(now);
Response.ContentType = contentType;
Response.Charset = "utf-8";
Response.Write(text);
Response.End(); // No caching if specified
The updated version of the JavaScriptResource handler that this code block is pulled from can be found in the West Wind Web Toolkit Repository here so you can compare with the original code from the original post.
Watch out for Response.End()
Now as it turns out the code as I have it written here DOES NOT work – as you can surmise from the highlighting the Response.End() actually makes this code run, but the output never makes it into the OutputCache. The reason for the Response.End() in the code here is that the code in question is a method in the handler rather than directly defined in ProcessRequest. Originally there was a bit of error checking code and cleanup that followed the output generation code in case there was a problem. There are also a number of error generation routines that create 404 requests on missing resources and errors on invalid syntax and so on – all of which also fire a Response.End(). So to be consistent I thought that using Response.End() everwhere would be the right thing to do.
Au contraire – using Response.End() in this fashion causes the processing of the pipeline to abort complete after output generation. In other words by specifying the Response.End() code at the end of the request causes UpdateRequestCache() to never fire and put the content into the cache. Response.End() (and HttpApplication.CompleteRequest()) causes the request to stop right at the point of the Response.End() call and jump to the EndRequest() handler, bypassing any events in between including UpdateRequestCache() which is needed to write the cache content into the OutputCache.
So, after a quick refactoring of the method and how post processing is handle I could remove the Response.End() call and voila the caching started working. The error method still do a Response.End() to exit early, but that’s acceptable because they should not cache anyway – they never actually hit the cache code (which is obviously important – you wouldn’t want to cache an error :-}).
This is pretty obvious once you know what’s happening, but I can tell you I looked at the code for quite some time agonizing over WHY the content was not getting into OutputCache even though it was staring me straight into the face. The Response.End() looked innocent enough.
Hopefully this post will help me – and maybe even one or two of you – jog my memory and avoid this problem in the future.
Other Posts you might also like