The conception, birth, and first steps of an application named Charlie

Subscribe: Atom or RSS

Static File Handlers

by Alister Jones (SomeNewKid)

In the second of the four weblog entries concerning Charlie’s cool URLs, I noted that Charlie uses a number of custom file handlers. The examples provided were a CSS Handler, a JavaScript Handler, a GIF Handler, and a JPEG Handler.

If you know a little bit about HTTP responses, you will know that there are three common response types. If the response is valid, it is a 200 OK response. If the file could not be found, it is a 404 Not Found response. If the file is found, but it has not been modified since the browser last requested it, it is a 304 Not Modified response. So that I did not have to duplicate this logic in each of my separate file handlers, I started by creating a base StaticHandler class. Here is the code.

using System;
using System.IO;
using System.Web;
using System.Security;

namespace Charlie.Framework.Interface.Handlers
{
   public abstract class StaticHandler : IHttpHandler
   {
   
      // Members required by IHttpHandler interface
   
      public void ProcessRequest(HttpContext context)
      {
         FileInfo fileInfo = new FileInfo(context.Request.PhysicalPath);
         if (IsFileValid(fileInfo) == false)
         {
            ReturnNotFoundResponse();
         }
         else if (IsFileModified(fileInfo, context) == false)
         {
            ReturnNotModifiedResponse(context);
         }
         else
         {
            ReturnFileResponse(fileInfo, context);
         }
      }
      
      public Boolean IsReusable
      {
         get
         {
            return true;
         }
      }
      
      // File Tests
      
      private Boolean IsFileModified(FileInfo fileInfo, HttpContext context)
      {
         Boolean isModified = true;
         String modifiedSince = context.Request.Headers["If-Modified-Since"];
         if (modifiedSince != null)
         {
            try
            {
               DateTime lastModified = fileInfo.LastAccessTimeUtc;
               DateTime lastReceived = Convert.ToDateTime(modifiedSince);
               if (lastModified == lastReceived)
               {
                  isModified = false;
               }
            }
            catch
            {
            }
         }
         return isModified;
      }

      private Boolean IsFileValid(FileInfo fileInfo)
      {
         if (File.Exists(fileInfo.FullName))
         {
            return true;
         }
         return false;
      }      
      
      // Responses

      private void ReturnNotFoundResponse()
      {
         throw new HttpException(404, "File not found.");
      }

      private void ReturnNotModifiedResponse(HttpContext context)
      {
         context.Response.StatusCode = 304;
         context.Response.SuppressContent = true;
      }
      
      private void ReturnFileResponse(FileInfo fileInfo, HttpContext context)
      {
         try
         {
            HttpResponse response = context.Response;
            this.ReturnFile(fileInfo, response);
            response.Cache.SetLastModified(fileInfo.LastWriteTime);
            response.Cache.SetCacheability(this.Cacheability);
            response.Cache.SetExpires(this.CacheExpiry);
         }
         catch (SecurityException)
         {
            throw new HttpException(401, "Access to file denied.");
         }
         catch
         {
            // it is more secure to not signfiy an error
            throw new HttpException(404, "Unable to serve file.");
         }
      }
      
      // Default Cache Values

      protected virtual HttpCacheability Cacheability
      {
         get
         {
            return HttpCacheability.Public;
         }
      }

      protected virtual DateTime CacheExpiry
      {
         get
         {
            return DateTime.Now.AddDays(1);
         }
      }
      
      // Abstract Member
      
      protected abstract void ReturnFile
            (FileInfo fileInfo, HttpResponse response);
   }
}

The code is very simple, and parts of it come from Milan Negovan’s Adding Variables To Style Sheets article. There is a single abstract ReturnFile method which a concrete handler class must implement. By creating this base class that forces a concrete handler to implement just one method, let’s see just how simple those handlers can be.

Here is the code for Charlie’s CSS Handler.

using System;
using System.IO;
using System.Web;

namespace Charlie.Framework.Interface.Handlers
{
   public class CssHandler : StaticHandler
   {
      protected override void ReturnFile
            (FileInfo fileInfo, HttpResponse response)
      {
         StreamReader reader = fileInfo.OpenText();
         String css = reader.ReadToEnd();
         reader.Close();
         response.Write(css);
         response.ContentType = "text/css";
      }
   }
}

Can’t get much simpler than that, can you?

Here is the code for Charlie’s JavaScript Handler.

using System;
using System.IO;
using System.Web;

namespace Charlie.Framework.Interface.Handlers
{
   public class JavaScriptHandler : StaticHandler
   {
      protected override void ReturnFile
            (FileInfo fileInfo, HttpResponse response)
      {
         StreamReader reader = fileInfo.OpenText();
         String script = reader.ReadToEnd();
         reader.Close();
         response.Write(script);
         response.ContentType = "text/javascript";
      }
   }
}

And here is the code for Charlie’s GIF Handler:

using System;
using System.IO;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;

namespace Charlie.Framework.Interface.Handlers
{
   public class GifHandler : StaticHandler
   {
      protected override void ReturnFile
            (FileInfo fileInfo, HttpResponse response)
      {
         Bitmap bitmap = new Bitmap(fileInfo.FullName);
         bitmap.Save(response.OutputStream, ImageFormat.Gif);
         bitmap.Dispose();
         response.ContentType = "image/gif";
      }
   }
}

The advanced developers in the audience will be thinking to themselves, “Well that’s fine, Alister, but the built-in StaticFileHandler would have done the same for you.” That’s true, but only because I have not yet updated the handlers to do anything special. But as a test, I updated the JPEG Handler to stamp a copyright notice on any returned JPEG image. Here is the code.

using System;
using System.IO;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;

namespace Charlie.Framework.Interface.Handlers
{
   public class JpegHandler : StaticHandler
   {
      protected override void ReturnFile
            (FileInfo fileInfo, HttpResponse response)
      {
         Bitmap bitmap = new Bitmap(fileInfo.FullName);

         // add copyright notice
         Font font = new Font("Verdana", 10);
         Brush brush = new SolidBrush(Color.Black);
         String copyright = "Copyright";
         Graphics canvas = Graphics.FromImage(bitmap);
         canvas.DrawString(copyright, font, brush, 0, 0);

         bitmap.Save(response.OutputStream, ImageFormat.Jpeg);
         bitmap.Dispose();
         response.ContentType = "image/jpg";
      }
   }
}

As you will see, the concrete handler classes are about as simple as you could want. Yet, they allow Charlie to have full control over every file used in a website. For example, tokens can be placed within a CSS style sheet (such as “[[CorporateColour]]”) and then used in a simple find-and-replace operation before the CSS file is returned. Another example was provided above, where a copyright notice is stamped on the JPEG image returned.

The only other thing to show is the code needed in the Web.config file that points the incoming file requests to the appropriate handler. To minimise the width of the code, I have replaced the real namespace with the word “Namespace”.

<system.web>
   <httpHandlers>
      <add verb="*" path="*.asxx" 
           type="Namespace.ViewHandler, Charlie.Framework"/>
      <add verb="GET" path="*.js" 
           type="Namespace.JavaScriptHandler, Charlie.Framework"/>
      <add verb="GET" path="*.css" 
           type="Namespace.CssHandler, Charlie.Framework"/>
      <add verb="GET" path="*.gif" 
           type="Namespace.GifHandler, Charlie.Framework"/>
      <add verb="GET" path="*.jpg" 
           type="Namespace.JpegHandler, Charlie.Framework"/>
      <add verb="GET" path="*.jpeg" 
           type="Namespace.JpegHandler, Charlie.Framework"/>
   </httpHandlers>
</system.web>

If you visit the little sample site that I have put online, keep in mind that Charlie is serving every single file used in that site—not just the two pages that are currently viewable. And if you want to do the same on your own website, you now have the code.

by Alister Jones | Next up: The WebHandler Object

2 comments

______
Blogger Julio Di Egidio said...  
 

Hello Alister,

unless I am missing something, there is a problem in the code above:

response.Write(css);
response.ContentType = "text/css";

AFAIK, those two statements should be reversed, because once you have started writing content, you cannot anymore set headers for the response.

The fact that it still works should be due to the web server (or the browser? maybe both) inferring the right content-type based on the file extension. In instance, if you simply had an .ashx extension, I think it wouldn't work reliably.

Ok, this said and my tech conscience satisfied, I just want to add that I like your articles a lot, because you make clear (and pleasant) what generations of deep experts don't even dare mentioning...

Thank you and keep up the good work! -LV

______
Blogger Alister said...  
 

Hi Julio,

The HttpResponse.Write method sends content to a buffered output stream. That stream is not sent to the client until HttpResponse.Flush or HttpResponse.End are called. So up until one of those finalizing methods are called, it is okay to set or change the HttpResponse.ContentType property.

I am reasonably sure of this, as I was careful to view the HTTP Headers sent by these static file handlers.

Thanks for your positive feedback. I appreciate it.

Post a Comment

----