views:

752

answers:

2

I'm trying to find an event that will fire immediately after all Application_Error event handlers so that I can modify the response sent (messing with the status code and 'location' headers and creating a new body specifically) using a custom HttpModule.

I've tried hooking into Application_EndRequest (as I've read that this is the only handler that is guaranteed to fire) but this is too late in the request processing to modify the response headers and I get a HttpException:

Server cannot append header after HTTP headers have been sent.
+2  A: 

I think ASP.Net launches a new response after ApplicationError, just for a different error-handling page. Try tacking into this response.

Update OK: Here's how to do it!

I wanted to make sure we can both attach an error handler and that we can gracefully handle the event in a page, so we can't make the last server error null, and we also want to be able to add headers!

In web.config, add:

<customErrors redirectMode="ResponseRewrite" mode="On">
    <error statusCode="500" redirect="~/ErrorHandler.aspx"/>
</customErrors>

Or whatever error specifically you're looking to catch. And under httpModules:

<add name="ErrorModule" type="TestErrorLifecycle.ErrorModule, TestErrorLifecycle"/>

The code to log the error is in this module:

public class ErrorModule : IHttpModule
{
    private volatile object locker = new object();

    public void Init(HttpApplication context)
    {
     context.Error += context_Error;
    }

    void context_Error(object sender, EventArgs e)
    {
     var app = sender as HttpApplication;

     lock (locker)
     {
      assertLogDirectory(app.Server);

      using (var fs = File.AppendText(app.Server.MapPath("~/logs/errorlog.txt")))
      {
       var lastException = app.Server.GetLastError();
       if (lastException == null) throw new ApplicationException("Not expected...");
       fs.WriteLine(lastException.Message);
      }
     }

     // we could also do a Request.Redirect() here...
    }

    private void assertLogDirectory(HttpServerUtility server)
    {
     var logdir = server.MapPath("~/logs/");
     if (!Directory.Exists(logdir))
      Directory.CreateDirectory(logdir);
    }

    public void Dispose()
    {
    }
}

I just wrote it to the filesystem. You could use kernel file transactions or an exclusive lock or whatever, but because I know I will only write to this file from here, I chose a simple private semaphore.

I added this in Default.aspx.cs just to test:

protected void Page_Load(object sender, EventArgs e)
{
    throw new ApplicationException("wtf, something wrong??!");
}

Created file ErrorHandler.aspx with this in it (some parts omitted for shortness):

<body>
    <h2>Error handler</h2>
    <form id="form1" runat="server">
     <p>Last error: <asp:Label ID="lblError" runat="server" /></p>
    </form>
</body>

And here's the page-class:

public partial class ErrorHandler : Page
{
    public ErrorHandler()
    {
     Load += ErrorHandler_Load;
    }

    private void ErrorHandler_Load(object sender, EventArgs e)
    {
     Response.AddHeader("X-TESTING", "Yes it works...");
     lblError.Text = Server.GetLastError() == null ? "noo, why is it null?!?" : Server.GetLastError().Message;
    }
}

Finally, I get these response headers at the ErrorHandler.aspx:

Server: ASP.NET Development Server/9.0.0.0
Date: Mon, 23 Feb 2009 00:37:45 GMT
X-AspNet-Version: 2.0.50727
X-TESTING: Yes it works...
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 725
Connection: Close
200 OK

If you try to add to Response.Headers-collection directly you need to run IIS in pipeline mode: http://forums.asp.net/p/1253457/2323117.aspx

I hope this helps! Cheers Henrik

Henrik
Have you got any pointers as to how one might go about this?
Kieran Benton
Yes, I'll give a test.
Henrik
A: 

Unless you move all error prone code prior to any of the Response being sent, you will not be able to solve this issue. If you want to do this, you need to set up an HttpHandler to work through having all of the code run prior to sending any information back to the client.

There are ways to re-architect to get info back to the client. Knowing where the application blows up and running it through try ... catch will help.

Gregory A Beamer
You're missing the point I'm afraid - this is not the case of me being unable to trap an exception, more that I need to be able to modify the error response back to the client.
Kieran Benton
I understand the point, but without altering the default implementation, you can't trap with a general error handler and alter the stream, as the headers are already sent. If you want to handle the ASP.NET pipeline yourself, you can do it.
Gregory A Beamer