views:

46

answers:

2

I have created an extension method called AddGZip which looks like the following:

public static void AddGZip(this HttpResponse response)
{
    response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
    response.AppendHeader("Content-Encoding", "gzip");
}

This is a very cut down version of the code:

var response = HttpContext.Current.Response;
var request = HttpContext.Current.Request;
var result = File.ReadAllText(path);
if (request.SupportsGZip)
{
  response.AddGZip();
}
response.Write(result);
response.Flush();

When you view the response in a web browser with GZip support you get an error like this:

"XML Parsing Error: unclosed token Location: http://webserver1/1234.xml Line Number 78, Column 1:"

When i view the source it's basically missed out the last > from the end of the XML file. So 1 or 2 bytes.

If I comment out the AddGZip Line it works fine. However I really want to support GZip as the XML can be quite large.

Does anyone have a suggestion for me? I've tried checking lots of blogs but no solution seems to be out there for this type of error.

Dave

A: 

Have you tried adding gzip through IIS? There is a question about it, so have a look what's it about. Basically, the IIS does all the compression so you don't have to.

veljkoz
+4  A: 

There is an issue (or perhaps a really clever feature that I haven't seen justified anywhere) with DeflateStream (GZipStream builds on DeflateStream and inherits the issue*), where flushing can lose data.

Response.Flush() will flush the filter. The solution is to use a wrapper that is aware of both the zipping and the underlying sink, and only flushes the latter:

public enum CompressionType
{
    Deflate,
    GZip
}
/// <summary>
/// Provides GZip or Deflate compression, with further handling for the fact that
/// .NETs GZip and Deflate filters don't play nicely with chunked encoding (when
/// Response.Flush() is called or buffering is off.
/// </summary>
public class WebCompressionFilter : Stream
{
    private Stream _compSink;
    private Stream _finalSink;
    public WebCompressionFilter(Stream stm, CompressionType comp)
    {
        switch(comp)
        {
            case CompressionType.Deflate:
                _compSink = new DeflateStream((_finalSink = stm), CompressionMode.Compress);
                break;
            case CompressionType.GZip:
                _compSink = new GZipStream((_finalSink = stm), CompressionMode.Compress);
                break;
        }
    }
    public override bool CanRead
    {
        get
        {
            return false;
        }
    }
    public override bool CanSeek
    {
        get
        {
            return false;
        }
    }
    public override bool CanWrite
    {
        get
        {
            return true;
        }
    }
    public override long Length
    {
        get
        {
            throw new NotSupportedException();
        }
    }
    public override long Position
    {
        get
        {
            throw new NotSupportedException();
        }
        set
        {
            throw new NotSupportedException();
        }
    }
    public override void Flush()
    {
        //We do not flush the compression stream. At best this does nothing, at worse it
        //loses a few bytes. We do however flush the underlying stream to send bytes down the
        //wire.
        _finalSink.Flush();
    }
    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotSupportedException();
    }
    public override void SetLength(long value)
    {
        throw new NotSupportedException();
    }
    public override int Read(byte[] buffer, int offset, int count)
    {
        throw new NotSupportedException();
    }
    public override void Write(byte[] buffer, int offset, int count)
    {
        _compSink.Write(buffer, offset, count);
    }
    public override void WriteByte(byte value)
    {
        _compSink.WriteByte(value);
    }
    public override void Close()
    {
        _compSink.Close();
        _finalSink.Close();
        base.Close();
    }
    protected override void Dispose(bool disposing)
    {
        if(disposing)
        {
            _compSink.Dispose();
            _finalSink.Dispose();
        }
        base.Dispose(disposing);
    }
}

It's also worth noting that most user-agents that support gzip-encoding also support deflate-encoding. While the size improvement with deflate is negliable (literally a few bytes), some libraries on some architecture deals with deflate considerably better (this goes for both compressing and decompressing), so it's always worth favouring deflate over gzip with HTTP compression.

Jon Hanna
Hi, Thanks for this.. I tried it out but it doesn't seem to make any difference. One thing that is weird is that my example and yours works OK on IIS 7.0 so i wonder if it's something not quite setup right on IIS 6.0? I changed my code to be response.Filter = new WebCompressionFilter(response.Filter, CompressionType.GZip);Any ideas?
CraftyFella
The above has worked for me on IIS 5 through 7. Tell me, if you comment out all calls to Response.Flush() does the problem still remain. Possibly we're fixing the wrong problem and it's not that flush issue at all.
Jon Hanna
Perhaps also check that Close() does get called (should happen with just about anything you might do). Finally, one advantage of having a directly implemented filter, is that you've now got somewhere to hook in some logging and make sure that everything that should have been written to it actually was, in case the source of the bug is actually somewhere else.
Jon Hanna
Hi, I tried removing the Response.Flush() and it works. Which is wicked, however I then loose the "Chunked Transfer-Encoding". Does that help narrow down to what the problem is?
CraftyFella
BTW - when you say you got it working for IIS5 and IIS6 did you have do the following: http://www.codinghorror.com/blog/2004/08/http-compression-and-iis-6-0.html
CraftyFella
Yeah, that seems to be the problem, but that is precisely the problem the above code is meant to resolve :( No, I didn't have to do anything with IIS6 beyond the above class. Nothing else is coming to me now, but maybe someone else will see something here, or an answer will occur to one of us soon.
Jon Hanna
no worries.. thanks any way.. I think i can live without "Chunked Transfer-Encoding" for now.. I've +1'd you :)
CraftyFella