ASP.NET MVC Custom Error Handling Application_Error Global.asax?
To answer the initial question "how to properly pass routedata to error controller?":
IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
Then in your ErrorController class, implement a function like this:
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Error(Exception exception)
{
return View("Error", exception);
}
This pushes the exception into the View. The view page should be declared as follows:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<System.Exception>" %>
And the code to display the error:
<% if(Model != null) { %> <p><b>Detailed error:</b><br /> <span class="error"><%= Helpers.General.GetErrorMessage((Exception)Model, false) %></span></p> <% } %>
Here is the function that gathers the all exception messages from the exception tree:
public static string GetErrorMessage(Exception ex, bool includeStackTrace)
{
StringBuilder msg = new StringBuilder();
BuildErrorMessage(ex, ref msg);
if (includeStackTrace)
{
msg.Append("\n");
msg.Append(ex.StackTrace);
}
return msg.ToString();
}
private static void BuildErrorMessage(Exception ex, ref StringBuilder msg)
{
if (ex != null)
{
msg.Append(ex.Message);
msg.Append("\n");
if (ex.InnerException != null)
{
BuildErrorMessage(ex.InnerException, ref msg);
}
}
}
I found a solution for ajax issue noted by Lion_cl.
global.asax:
protected void Application_Error()
{
if (HttpContext.Current.Request.IsAjaxRequest())
{
HttpContext ctx = HttpContext.Current;
ctx.Response.Clear();
RequestContext rc = ((MvcHandler)ctx.CurrentHandler).RequestContext;
rc.RouteData.Values["action"] = "AjaxGlobalError";
// TODO: distinguish between 404 and other errors if needed
rc.RouteData.Values["newActionName"] = "WrongRequest";
rc.RouteData.Values["controller"] = "ErrorPages";
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(rc, "ErrorPages");
controller.Execute(rc);
ctx.Server.ClearError();
}
}
ErrorPagesController
public ActionResult AjaxGlobalError(string newActionName)
{
return new AjaxRedirectResult(Url.Action(newActionName), this.ControllerContext);
}
AjaxRedirectResult
public class AjaxRedirectResult : RedirectResult
{
public AjaxRedirectResult(string url, ControllerContext controllerContext)
: base(url)
{
ExecuteResult(controllerContext);
}
public override void ExecuteResult(ControllerContext context)
{
if (context.RequestContext.HttpContext.Request.IsAjaxRequest())
{
JavaScriptResult result = new JavaScriptResult()
{
Script = "try{history.pushState(null,null,window.location.href);}catch(err){}window.location.replace('" + UrlHelper.GenerateContentUrl(this.Url, context.HttpContext) + "');"
};
result.ExecuteResult(context);
}
else
{
base.ExecuteResult(context);
}
}
}
AjaxRequestExtension
public static class AjaxRequestExtension
{
public static bool IsAjaxRequest(this HttpRequest request)
{
return (request.Headers["X-Requested-With"] != null && request.Headers["X-Requested-With"] == "XMLHttpRequest");
}
}
Instead of creating a new route for that, you could just redirect to your controller/action and pass the information via querystring. For instance:
protected void Application_Error(object sender, EventArgs e) {
Exception exception = Server.GetLastError();
Response.Clear();
HttpException httpException = exception as HttpException;
if (httpException != null) {
string action;
switch (httpException.GetHttpCode()) {
case 404:
// page not found
action = "HttpError404";
break;
case 500:
// server error
action = "HttpError500";
break;
default:
action = "General";
break;
}
// clear error on server
Server.ClearError();
Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));
}
Then your controller will receive whatever you want:
// GET: /Error/HttpError404
public ActionResult HttpError404(string message) {
return View("SomeView", message);
}
There are some tradeoffs with your approach. Be very very careful with looping in this kind of error handling. Other thing is that since you are going through the asp.net pipeline to handle a 404, you will create a session object for all those hits. This can be an issue (performance) for heavily used systems.