Getting message from exception thrown by an ASP.NET MVC controller action called from jQuery ajax
I am trying to secure a controller action that is being called with the jQuery ajax function.
In my action method I have the code surrounded by a try-catch block, so I capture all my exceptions and return a JSON result with the exception message. To that point everything works great.
The problem begins when I throw an exception outside the try-catch block, for example, if the exception is raised inside a filter action attribute. In that case I cannot return a JSON result because the flow stops abruptly.
jQuery catches that exception in the error callback function. But the only place where I have managed to see the exception message is in the xhr.responseText, but it contains the whole "yellow page of death" error from asp.net.
The very ugly and hackish solution I ha开发者_如何学Gove used to get the exception message is to extract the text between the <title> tag. But I really hope there is a better way to do this!!
What would you do in this scenario? How would you secure your ajax actions without writing that logic inside the action method? How would you show to the user the message from an unhandled exception thrown by an ASP.NET MVC controller action called from jQuery ajax?
jQuery Ajax Call:
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: url + "?id=" + id,
success: function(data) {
if(data.success){
alert('Success');
} else {
alert('Fail: ' + data.message);
},
error: function(xhr, status, err) {
// There has to be a better way to do this!!
var title = xhr.responseText.split("<title>")[1].split("</title>")[0];
alert(title);
}
});
Controller Action:
[MyAttribute]
public ActionResult MyAction(int id)
{
try
{
// Do something
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
catch (Exception exception)
{
return Json(new { success = false, message = exception.Message }, JsonRequestBehavior.AllowGet);
}
}
Action Filter Attribute:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// This causes a 401 Unauthorized HTTP error.
throw new UnauthorizedAccessException("Access Denied.");
}
You could use the Application_Error
method in Global.asax:
protected void Application_Error(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
HttpContext context = app.Context;
Exception ex = context.Server.GetLastError();
bool isAjaxCall = string.Equals(
"XMLHttpRequest",
context.Request.Headers["x-requested-with"],
StringComparison.OrdinalIgnoreCase
);
if (isAjaxCall)
{
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json";
var json = new JavaScriptSerializer().Serialize(new { error = ex.Message });
context.Response.Write(json);
}
else
{
// TODO: Handle the case of non async calls
}
}
Maybe you can implement a inherent class from the HandleError attribute and make it return a Json on any exception, I'm going to check MVC code and edit this answer later.
* Edit * Check this class.
public class ErrorHandlingJSon : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
/***** Original code from MVC source ******/
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)
{
return;
}
Exception exception = filterContext.Exception;
// If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
// ignore it.
if (new HttpException(null, exception).GetHttpCode() != 500)
{
return;
}
if (!ExceptionType.IsInstanceOfType(exception))
{
return;
}
//***** This is the new code *****//
if (filterContext.HttpContext.Request.IsAjaxRequest()) // If it's a ajax request
{
filterContext.Result = new JsonResult // Set the response to JSon
{
Data = new { success = false, message = exception.Message }
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 200; // Maybe it should be 500, but this way you handle the JQuery on the success event
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
else //*** From here, is the original code againg **//
{
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult
{
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
// Certain versions of IIS will sometimes use their own error page when
// they detect a server error. Setting this property indicates that we
// want it to try to render ASP.NET MVC's error page instead.
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
I used the same code from the MVC source, but add a different response when the request is Ajax, so it return a JSon result. I set the status code to 200, so you handle the exception on the JQuery success option. If you return a 500, then you should handle the exception on the error option, maybe this is a better way to do it.
For this to work just use the [ErrorHandlingJSon] on top of your controller. You have to set the web config custom error to on. There is a line on the attribute where it check if the customErrors is on or off, you can return here the Json so it works when customErrors is off.
精彩评论