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

4

6

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.sstatic.net/R3Wmq.png

Adventurous answered 12/1, 2012 at 23:5 Comment(0)
T
1

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.

Tricksy answered 5/5, 2014 at 17:38 Comment(0)
P
0

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?

Pola answered 13/1, 2012 at 2:14 Comment(7)
So when an error occurs, the handler removes any headers and content that have been written (but not yet flushed), but neglects to remove the filter I've put in place?Adventurous
Yes. It's irritating, but I think filter was meant to sit conceptually at a higher level in the pipeline, so it made sense to someone...Pola
It seems to me that if the error handler removes the headers I've written then it should also remove the Filters I've assigned. I wonder if this is a bug, or by design... Otherwise, can you clarify what you mean by "the 200 status you were going to send is wrong"? The exception handler is what sets the 500 status. Why should I manually set a status other than 200?Adventurous
I'm not convinced it was the right decision that filters remain either. By "the 200 you were going to send" I mean the 200 that you would have sent if the exception hadn't happened.Pola
If the exception hadn't happened, wouldn't that mean that 200 was correct?Adventurous
Yes, which means there's at least one piece of header information set that is no longer correct, was my point.Pola
I don't follow you. What status should I be setting, exactly? If you want to edit your answer to clarify what you mean and summarize the fact that the handler removes any headers and content that have been written (but not yet flushed), but neglects to remove the filter, then I'll accept your answer. As it is, I'm confused by the talk of the 200 status being wrong.Adventurous
D
0

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;
        }
    }
}
Degeneration answered 27/11, 2013 at 21:13 Comment(0)
M
-1

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.

Monongahela answered 13/1, 2012 at 0:3 Comment(7)
So if you execute my code as-is, you get a proper yellow-screen-of-death, rather than still-compressed gobbledygook?Adventurous
yes, as-is, I make copy/paste and I get a proper yellow-screen-of-death. The problem appears when I flush() !!!!!!!! do you flush somewhere before the error ? The gzip not appear, but the filter not set also.Monongahela
No, I'm not manually flushing anywhere in my test case. Can you verify that your browser is sending an "Accept-Encoding:gzip" header?Adventurous
@DavidMills yes of course I have verify it, I make the test with and with out the exception. I use the google chrome the last version. With out the exception I see the gzip, with the exception I do not see the gzip.Monongahela
Hmm. What version of the .NET framework are you on? I happen to be testing with Visual Studio 2008 and .NET 3.5. Maybe something has changed for .NET 4?Adventurous
I am vs 2010, net 4, tested now on iis 5.1 How ever I am programming from net2 and never remember to have this issue.Monongahela
I just tried it on .NET 4, but I still see the same issue. I'm unsure why we can't reproduce the issue on your machine.Adventurous

© 2022 - 2024 — McMap. All rights reserved.