Three pitfalls to avoid when writing a response filter

22. July 2011 22:06 by Matt Wrock in   //  Tags:   //   Comments

I was looking at a response filter that someone else had written yesterday and I noticed a few things it was doing that ideally you want to avoid in a response filter. This is a particularly fresh topic for me since I am nearing the end of V1 development on a response filter that will automatically find a response's css, merge them, find their background images, sprite the ones it can then create a new and minified css with these sprites. I'll be blogging much more on that next month.

Now, in order to write a good filter that will work with any site and be performant is not particularly easy. If your filter is limited to a small or smaller sites, this advise may be considered to lie in the category of preoptimization. But real quick...before I elaborate on these pitfalls...

What is a response filter?

A response filter is simply a class that derives from System.IO.Stream. This class is attached to an HttpResponse's Filter property like so:

Response.Filter = new MyFilter(HttpContext.Current.Response.Filter, 
    HttpContext.Current.Response.ContentEncoding);

As the underlying response outputs to its OutputStream, this output is sent to the filter which has the opportunity to examine and manipulate the response before it gets to the browser. The filter does this by overriding Stream's Write method:

void Wite(byte[] buffer, int offset, int count);

When the filter is ready to send its transformed response to the browser or just forward the buffer on unchanged, it then calls the underlying stream's write method. So your filter might have code like this:

        public ResponseFilter(Stream baseStream, Encoding encoding)
        {
            this.encoding = encoding;
            BaseStream = baseStream;
        }

	protected Stream BaseStream { get; private set; }

        public override void Write(byte[] buffer, int offset, int count)
        {
		var header = encoding.GetBytes("I am wrapping");
		var footer = encoding.GetBytes("your response");
		BaseStream.Write(header, 0, header.Length);
		BaseStream.Write(buffer, offset, count);
		BaseStream.Write(footer, 0, footer.Length);
         }

This is a common implementation used for adding compression to a site or ensuring that a site's content is always wrapped in a common header and footer.

So with that background, here are some things to try and avoid in a solid filter: 

Assuming UTF-8

This is easy to overlook and honestly it will work most of the time, but if you think that your filter will ever be dropped on a Japanese website, or a website that is intended to be localized to a double byte unicode locale you might be disapointed. Very disapointed. Avoid doing something like this:

BaseStream.Write(encoding.GetBytes("I am wrapping"), 0, 
    "I am wrapping".Length);

In a Japanese locale, the underlying encoding will be unicode and the length of the byte array will be twice the size of  "I am wrapping".Length which is likely UTF-8. So the users see just half the stream. But thats ok, the first half was way better.

Copying the buffer to a string

 You might be tempted to do something like this:

public override void Write(byte[] buffer, int offset, int count)
{
    var output = encoding.GetString(buffer, offset, count)
    var newOut = encoding.GetBytes("header" + output + "footer");
    BaseStream.Write(newOut, 0, newOut.Length);
}

You have now managed to double the memory footprint of the original response by copying it to a new variable. This can be a sensitive issue with filters since they often process almost ALL output in a site. Unfortunately, if you need to do alot of text searching and replacing on the original byte array and you want to be efficient, this can be difficult and tedious code to write, read and test. I intend to devote a future post to this topic exclisively.

Ignoring the offset and count parameters

You might think that using the offset and count parameters in your Write override is not necessary. After all, you are confident that your transformations can go to the browser as is because you don't have any code that would need to do further processing on the buffer. Well maybe you don't but someone else might. You may have no control over the fact that someday another HttpModule will be added to the site that registers another filters. Response filtering fully supports the ability to chain several filters together. Someone elses module might have have the code mentioned above in their own class:

Response.Filter = new MyFilter(HttpContext.Current.Response.Filter, 
    HttpContext.Current.Response.ContentEncoding);

So if this is called after your own filter was added to the response, then YOU are HttpContext.Current.Response.Filter. That new filter might do something like:

public override void Write(byte[] buffer, int offset, int count)
{
    int[1] headBoundaryIndexes = FindOpeningHead(buffer, offset, count);
    BaseStream.Write(buffer, 0, headBoundaryIndexes[0]);
    BaseStream.Write(anEvenBetterHead, 0, anEvenBetterHead.Length);
    BaseStream.Write(buffer, headBoundaryIndexes[1], (offset + count) - headBoundaryIndexes[1]);
}

So if your filter is this filter's BaseStream and your Write looks like this:

public override void Write(byte[] buffer, int offset, int count)
{
    var output = Encoding.UTF8.GetString(buffer);
    var newOut = output.Replace("super", "super duper");
    BaseStream.Write(Encoding.UTF8.GetBytes(newOut), 0, newOut.Length);
}

Ouch. Your users are probably looking at something other than what you intended. The upstream filter was trying to replace the head but now they three. After several years in the industry and meticulous experimentation, I have found that 1 is the perfect number of heads in a web page.

Oh and look, this code managed to violate all three admonitions in one blow.

blog comments powered by Disqus

About Me

Hey thats me!

I'm Matt Wrock with over thirteen years of experience architecting scalable, distributed, high traffic web applications. I currently live in Woodinville, WA with my wife, two daughters, three dogs and cat. I work for Microsoft as a Sr. Software Engineer working in Cloud Developer Services. I'm also project founder and owner of http://www.requestreduce.org and a committer to http://chocolatey.org.

Month List