Whenever we google for ASP.Net MVC exception handling techniques, we get a bunch of links but most of those explains about different techniques and finally ask us to decide one method based on our requirement. Sometimes we will end up in a more confused state. This article is to help a real time ASP.Net MVC developer to decide his logging mechanism.
Aim of error handling
Below are the things we should aim when we develop a error handling framework.
Never expose the exception details
We should never return the details of exception such as call stack to the end user. If we expose, it is considered as a security hole for hackers to understand the working of our system.
Log all the exception details
No system in this world is perfect. We need to accept the fact that, there may be exceptions. How we are dealing with those exceptions is important. To fix bugs and improve our system, it is required to log all the exceptions occurred in the system. The developers can therefore analyse the logs and fix.
Types of errors
Below are the different types of error conditions, we can expect in our ASP.Net MVC application.
Exceptions in ASP.Net MVC pages
This is the most common type of exceptions we need to handle. There may be exceptions when we execute the controller code which needs to be handled properly.
How to handle MVC page exceptions
There are 2 methods to handle this. Either we need to handle the exception at MVC framework level using the HandleError attribute on the controller classes or handle the exception at application level by global.asax :Application_Error().
I would suggest going for the error handling at the application level. It will capture all the error including routing issues. How that is useful in case of wrong URLs can be seen in next section.
One challenge in handling the exceptions at application level is that we cannot show the MVC Error page directly as we are out of MVC framework. For that we need to follow the below technique in the Application_Error method.
protected void Application_Error(object sender, EventArgs e) { var httpContext = ((MvcApplication)sender).Context; new MVCApplicationExceptionHandler().Handle(httpContext,Server); }
public class MVCApplicationExceptionHandler { internal void Handle(HttpContext httpContext, HttpServerUtility server) { var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext)); string currentControllerName = GetCurrentControllerName(currentRouteData); string currentActionName = GetCurrentActionName(currentRouteData); var ex = server.GetLastError(); new ExceptionLogger().Log(ex); RedirectToErrorPage(httpContext, ex,currentControllerName,currentActionName); } private void RedirectToErrorPage(HttpContext httpContext,Exception ex, string currentControllerName, string currentActionName) { //Clearing httpContext httpContext.ClearError(); httpContext.Response.Clear(); httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500; httpContext.Response.TrySkipIisCustomErrors = true; //Setting values for new route string errorAction = GetErrorActionNameFrom(ex); var routeData = new RouteData(); routeData.Values["controller"] = "Error"; routeData.Values["action"] = errorAction; //Fire the ErrorController var controller = new ErrorController(); controller.ViewData.Model = new HandleErrorInfo(ex, currentControllerName, currentActionName); ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData)); } private string GetErrorActionNameFrom(Exception ex) { var action = "Index"; var httpEx = ex as HttpException; if (httpEx !=null) { switch (httpEx.GetHttpCode()) { case 404: action = "NotFound"; break; default: action = "Index"; break; } } return action; } private string GetCurrentActionName(RouteData currentRouteData) { string actionName = string.Empty; if (currentRouteData != null && currentRouteData.Values["action"] != null && !String.IsNullOrEmpty(currentRouteData.Values["action"].ToString())) { actionName = currentRouteData.Values["action"].ToString(); } return actionName; } private string GetCurrentControllerName(RouteData currentRouteData) { string controllerName = string.Empty; if (currentRouteData != null && currentRouteData.Values["controller"] != null && !String.IsNullOrEmpty(currentRouteData.Values["controller"].ToString())) { controllerName = currentRouteData.Values["controller"].ToString(); } return controllerName; } }
The ErrorController and it's View can be simple. ErrorController.Index() action which will be invoked when an exception occurs inside the controller.
Requests to wrong ASP.Net MVC page URLs which cannot be routed
Sometimes there will be requests to wrong URLs. We need to respond with proper message instead of showing the white screen or ASP.Net generated error page.
eg: www.mycompany.com/Emplyee/1 - The spelling mistake in the word 'employee' should be handled by our application.
How to handle wrong MVC URL requests
The above solution will work for wrong URLs as well. In this case the NotFound action will be invoked.See the attached sample for more details.
Exceptions in AJAX calls
AJAX calls may also raise exception in the controller. Those needs to be handled and the required information needs to be passed to the client side to inform the user about his request.
How to handle AJAX exceptions
Instead of error page we should return a JSON response which tells that there is an error happened. Lets see one example below
<script type="text/javascript"> function divide() { var n1 = $("#n1").val(); var n2 = $("#n2").val(); //Lets not bother about the data which is available in n1 and n2. Assume that those are numbers $.ajax({ type: "GET", cache: false, url: "../Calculator/Divide", data: { "n1": n1, "n2": n2 }, // multiple data sent using ajax success: function (html) { if (html.IsSuccess) { $("#res").val(html.Result); } else { alert(html.Result); } }, error: function (a, b, c) { alert("Some unexpected error happened"); } }); } </script>
public class CalculatorController : Controller { public ActionResult Divide(int n1,int n2) { try { return new JsonResult() {JsonRequestBehavior = JsonRequestBehavior.AllowGet,Data = new { IsSuccess = true, Result = n1 / n2 } }; } catch (DivideByZeroException ex ) { return new JsonResult() {JsonRequestBehavior = JsonRequestBehavior.AllowGet, Data = new { IsSuccess = false, Result = "You cannot divide by Zero" } }; } } }
I don't think it needs any explanation. Even in case of exception its returning the JSON response. But before processing at the client side, it should check for IsSuccess property. If any other exception occurs the control goes to application level which we can see in next section.
The catch blocks should not be exception eating blocks. Use the catch blocks which we are sure that we can handle.
AJAX Request to wrong URLs which cannot be routed
In case of an AJAX request to wrong url either, we should return JSON response with error details or return "Not found status". Here we are redirecting to ErrorController.APINotFound() action from there we are returning JSON result. The response http code will still be error. Only advantage here is that the clients will get details of the error or what went wrong in a secure manner. This is the better method which I could see instead of throwing http errors alone.
How to handle wrong AJAX requests
For this we had to modify one of the method in earlier code which is used to retrieve the Error action name. Now we need to look into the request header for the origin. If its originated from AJAX the value will be "XMLHttpRequest".
private string GetErrorActionNameFrom(Exception ex, HttpContext context) { var action = "Index"; if (!string.IsNullOrWhiteSpace( context.Request.Headers["X-Requested-With"]) && context.Request.Headers["X-Requested-With"].Equals("XMLHttpRequest")) { action = "APINotFound"; } else { var httpEx = ex as HttpException; if (httpEx != null) { switch (httpEx.GetHttpCode()) { case 404: action = "NotFound"; break; default: action = "Index"; break; } } } return action; }
public ActionResult APINotFound() { return new JsonResult() { JsonRequestBehavior = JsonRequestBehavior.AllowGet, Data = new { IsSuccess=false,Result="Not able to process your API request due to wrong url or unexpected errors"} }; }
function divide() { var n1 = $("#n1").val(); var n2 = $("#n2").val(); //Lets not bother about the data which is available in n1 and n2. Assume that those are numbers $.ajax({ type: "GET", cache: false, url: "../Calculator/ivide", data: { "n1": n1, "n2": n2 }, // multiple data sent using ajax success: function (html) { if (html.IsSuccess) { $("#res").val(html.Result); } else { alert(html.Result); } }, error: function (a, b, c) { alert(JSON.parse(a.responseText).Result); } }); } </script>
Sample can be download from sky drive. Now we can say out ASP.Net MVC application has an exception handling framework.
No comments:
Post a Comment