Use GZipStream as response in plain Asp.Net Core 5

Recently, I’m struggle with asp.net core 5 working with GZipStream. My request is easy: using GZipStream to compress a large text before sending it as response of asp.net content.

Here is my first guess code, which is not working:

//This code is not working.
public static async Task WriteWithGZipAndCompleteAsync(this HttpResponse response, string text)
{
    response.StatusCode = 200;
    response.ContentType = "application/gzip";

    await using var gzip = new GZipStream(response.Body, CompressionLevel.Optimal, true);
    await using var streamWriter = new StreamWriter(gzip, Encoding.UTF8, -1, true);

    await streamWriter.WriteAsync(text);
    await streamWriter.FlushAsync();
    streamWriter.Close();

    await gzip.FlushAsync();
    gzip.Close();

    await response.CompleteAsync();
}

It does send the text to the client but the gunzip reports “unexpected end of file” after decompressed all texts.

The second version is worse, it sends nothing at all:

//This code is not working either.
public static async Task WriteWithGZipAndCompleteAsync(this HttpResponse response, string text)
{
    response.StatusCode = 200;
    response.ContentType = "application/gzip";

    using (var compressed = new MemoryStream())
    {
        using (var gzip = new GZipStream(compressed, CompressionLevel.Optimal, true))
        {
            var uncompressed = Encoding.UTF8.GetBytes(text);
            gzip.Write(uncompressed);

            gzip.Flush();
            gzip.Close();
        }

        var result = compressed.ToArray();
        response.Body.Write(result);
    }

    await response.CompleteAsync();
}

Finally, I get it work by this code below:

public static async Task WriteWithGZipAndCompleteAsync(this HttpResponse response, string text)
{
    response.StatusCode = 200;
    response.ContentType = "application/gzip";

    await using var gzip = new GZipStream(response.Body, CompressionLevel.Optimal, true);
    await using var streamWriter = new StreamWriter(gzip, Encoding.UTF8, -1, true);

    await streamWriter.WriteAsync(text);
    await streamWriter.FlushAsync();
    await gzip.FlushAsync();
    await response.Body.FlushAsync();

    //WARNING: DO NOT CALL CompleteAsync, which will thrown an exception.
}

Conclusion:

  • When calling Close() on instances of StreamWriter and GZipStream, the underlying stream will be closed, NO MATTER the value of leaveOpen specified. Uhh? Weird? But it’s true.
  • Do not call response.CompleteAsync() after write data to response.Body.
    • If Close() called on streams before, calling response.CompleteAsync() will thrown an ObjectDisposedException: Cannot access a closed stream.
    • If Close() does not present, like the code above, calling response.CompleteAsync() will thrown an InvalidOperationException: Writing is not allowed after writer was completed.