"Content-encoding" header disappears from HttpHandler response if an exception occurs

4k Views Asked by At

I have a custom HttpHandler in which I manually enable output compression, like so:

context.Response.AppendHeader("Content-encoding", "gzip");
context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);

This works nicely for most requests, but when an exception is encountered the "Content-encoding" header disappears from the response, while the compression filter remains in place. The result is that the error page is gzip compressed, but the browser receives no header indicating that fact. The browser then tries to display the still-compressed data as text, which is gobbledygook.

Full test case code is shown below. Try alternately disabling the compression or not throwing the exception.

Can anyone shed some light on why the "Content-encoding" header disappears?

I suppose I could simply enable compression as the last thing the handler does, so that if an exception is encountered it never reaches the point where the compression filter is added; but the behavior I'm seeing strikes me as a bug. Can anyone confirm?

public class TestHandler : IHttpHandler 
{
    public void ProcessRequest(HttpContext context)
    {
        CompressResponse(context);
        context.Response.Write("Hello world");

        // Throw an exception for testing purposes
        throw new Exception("Just testing...");
    }

    private void CompressResponse(HttpContext context)
    {
        string acceptEncoding = context.Request.Headers["Accept-Encoding"];
        if (String.IsNullOrEmpty(acceptEncoding))
        {
            return;
        }

        // gzip or wildcard
        if (acceptEncoding.ToLower().Contains("gzip") || acceptEncoding.Contains("*"))
        {
            context.Response.AppendHeader("Content-encoding", "gzip");
            context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
            return;
        }

        // Also handles deflate (not shown here)
        // <snip>
    }

    public bool IsReusable
    {
        get { return true; }
    }
}

EDIT: Screenshot of the still-encoded response I'm seeing with my test case: https://i.stack.imgur.com/R3Wmq.png

4

There are 4 best solutions below

7
On

If you have an exception, then the server will flush the currently set headers and content, because they're wrong, as you did after all have an exeption.

At the very least, it's clear that the 200 status you were going to send (because all successful responses that don't change the status send a 200, and upon an unhandled exception it was no longer succesful) is wrong, but everything else related to something you were going to do but failed to achieve, so it's all wrong and it all goes.

Filters aren't reset though.

Either reset the headers in the error page if appropriate, or don't set the filter unless you can be sure that everything is ready to flush. I'd go for the former, no reason why error pages can't be compressed too.

You can't send a header if you've called Flush(), because well, because you've flushed. Where's the header going to go?

7
On

I test your code and I can not find any issue. Yes the gzip is not set, but the filter also not have been set and asp gets the control and send an error.

Forcing the header to flush make a real problem

 CompressResponse(context);
 context.Response.Flush(); 

If I force the gzip header then the page is not render correctly.

Two thinks here maybe is your issue. You do not have set the page encoding

context.Response.ContentEncoding = new UTF8Encoding();

and you do not have set ContentType

context.Response.ContentType = "text/plain";

Maybe some of this is the reason that you get not corrected page render. How ever in my tests even with that the issue you describe not appears.

0
On

I had the same thing happen when forcing gzip on a WebForms application. In order to fix it I had to clear the filter in the Application_Error method in Global.asax.cs

protected void Application_Error(Object sender, EventArgs e)
{
    Response.Filter = null;
}

The reason this is happening is b/c the filter is being set before the app has an error. And for some reason the yellow screen error message clears the Content-encoding header but doesn't do anything to the response filter.

0
On

I encountered this problem as well. It was complicated to track down. I am unsure of the exact specifics of this whole situation, but what I think happens is that there is a memory leak.

When you first do this:

context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);

you are assigning an unmanaged resource to Filter. Normally this would be wrapped in a using statement so that it would be properly disposed in case anything went wrong.

So, when something does go wrong, there is a problem. The Filter contains a stream which is still open even though the response is being written to with the yellow screen of death. What ensues is madness (as shown in your screen shot).

Fear not! There is actually an easy way to overcome this issue. Dispose of the filter. Luckily, there is already a place to apply this global check for filter disposal.

global.asax.cs

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
        filters.Add(new HandleErrorAttribute());//default handler
        filters.Add(new HandleErrorEncodingAttribute());//extra check for filter disposal
}

error handler namespace

public class HandleErrorEncodingAttribute : FilterAttribute, IExceptionFilter
{
    public virtual void OnException(ExceptionContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }
        if (filterContext.IsChildAction)
        {
            return;
        }
        // If custom errors are disabled, we need to let the normal ASP.NET exception handler
        // execute so that the user can see useful debugging information.
        if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
        {
            filterContext.HttpContext.Response.Filter.Dispose();//fixes response stream
            return;
        }
    }
}