In ASP.Net MVC 2 I am using the following compression filter and, in Chrome it works fine but in Firefox 3.3.6 it returns weird characters.
public class CompressAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//get request and response
var request = filterContext.HttpContext.Request;
var response = filterContext.HttpContext.Response;
//get requested encoding
if (!string.IsNullOrEmpty(request.Headers["Accept-Encoding"]))
{
string enc = request.Headers["Accept-Encoding"].ToUpperInvariant();
//preferred: gzip or wildcard
if (enc.Contains("GZIP") || enc.Contains("*"))
{
response.AppendHeader("Content-encoding", "gzip");
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
//deflate
else if (enc.Contains("DEFLATE"))
{
response.AppendHeader("Content-encoding", "deflate");
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
}
base.OnActionExecuting(filterContext);
}
}
Here are a sample of the characters displayed by Firefox:
��������I�%&/m�{J�J��t��$ؐ@�����iG#)�*��eVe]f@�흼��{���{��;�N’���?\fdl��J�ɞ!���?
What is the cause?
A few things that I’ve found cause problems with rolling your own compression.
First. Some situation causes a complete change to how the response is handled (Server.Transfer, an HTTP module deferring to another HTTP module) may clear the headers, but keep the stream. Fiddler will quickly tell you if this is the case. One possibility is that this happens when you go to your error response, and an error is happening in the FF case. Forcibly decompressing the stream yourself should help diagnose here.
Conversely, a sequence of events could have led to the headers and/or the compression being doubled-up, so you end up sending a gzip of a gzip and similar. Worse yet, the filter may have been changed part-way through the response.
Third. Just putting in a DeflateStream or GZipStream as the filter does not correctly handle the case where chunked encoding is used (buffering is off, HttpResponse.Flush() is called, or a response larger than the largest allowed buffer size is sent). The following stream class handles this case correctly (it’s the override of
Flush()that does the fix, the extra public properties I have found useful in dealing with the cases described above).Fourth. With a content-encoding (rather than a transfer-encoding) HTTP considers you to be actually sending a different entity than with a different encoding. (Transfer-encoding considers you to just be using the encoding so that there’s less bandwidth used, which is what we normally really want, but alas support for Transfer-encoding isn’t as common, so we kludge by using Content-Encoding instead). As such you need to make sure that e-tags (if there are any) are different between the different encodings (adding a G for gzip and an D for default before the last ” character should do the trick, just don’t repeat mod-gzip’s bug of putting it after the ” character).
Fifth. Related to this, you must send an appropriate Vary header given that you can vary according to content-encoding. Doing this correctly means sending Vary: Accept-Encoding, to indicate that what you send will depend on the value of that heading. Because this causes issues with IE (thankfully the next version will have some improvement, according to MS) some people send Vary: User-Agent instead (on the basis that most user-agents either accept compression content-encodings or don’t, rather than requesting sometimes and not others). Note that you need to set the Vary header when you are prepared to compress, even in cases where you don’t.
Sixth. Even if you’re doing everything perfectly, something in the cache from earlier in your development can mess with it, as you’ve just changed the rules for caching after it got cached. Clear your cache.
If none of those fit the bill, at least do look at what you see in a tool like Fiddler, and what you see if you manually decompress the stream sent to FF, it should definitely help.
Incidentally, your code above favours GZip over Deflate, whatever the client preference is. If I was going to ignore client-stated preference order, I’d do it the other way around. Since GZip is built on Deflate, the GZip is always slightly larger than the Deflate. This difference is negliable, but more importantly some implementations will take much more CPU-time to work with g-zip data than deflate data, and this depends on architecture as well as software (so just testing on one machine doesn’t tell you enough to judge if this applies), so for a client running their browser on a low-end machine, the appreciable difference between gzip and deflate might be more than just downloading the few extra octets gzip will send.