PDA

View Full Version : Global AJAX error handling in MVC Controllers



anup
Jan 06, 2015, 11:17 AM
Hi,

I think the following could be a useful implementation to handle unexpected errors globally inside an MVC Controller.

The idea is that by default any uncaught exceptions in a controller will redirect to your configured error page (assuming you have set that up). That may be fine for Controllers that are returning Views.

But if your Controllers are returning JSON, such as Ext.NET DirectResult instances, then you may not want a redirect to the default error page, especially if you have set up error handling in your Direct Method invocation or global AJAX error handling in your JavaScript layer, etc.

So, to handle that, we can extend the default HandleErrorAttribute and register it as a global filter:



//
// Idea courtesy http://stackoverflow.com/questions/12705345/mvc3-return-json-on-error-instead-of-html
//
public class HandleAndLogErrorAttribute : HandleErrorAttribute
{
// I am using Log4Net for logging purposes, so getting a logger here
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod() .DeclaringType);

public override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled)
return;

Log.Error("Unhandled exception", filterContext.Exception);

if (RequestManager.IsAjaxRequest)
{
filterContext.Result = new DirectResult
{
Success = false,
ErrorMessage = Resources.UnexpectedError
};

// Note, Direct Method exceptions currently expect HTTP Status Code of 200, not 500
// More info: http://forums.ext.net/showthread.php?49481-Can-DirectResponse-set-the-http-status-code-to-500-in-case-of-errors
UpdateFilterContext(filterContext, (int)HttpStatusCode.OK);
}
else if (filterContext.HttpContext.Request.IsAjaxRequest() )
{
// This section (if needed) would be for any AJAX requests that are
// not via Ext.NET - you could remove this if you don't need it
// In addition, you may want to change the Data to suit your needs
filterContext.Result = new JsonResult
{
Data = new { success = false, error = Resources.UnexpectedError },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};

UpdateFilterContext(filterContext);
}
else
{
base.OnException(filterContext);
}
}

private static void UpdateFilterContext(ExceptionContext filterContext, int statusCode = (int)HttpStatusCode.InternalServerError)
{
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = statusCode;
filterContext.HttpContext.Response.TrySkipIisCusto mErrors = true;
}
}


Then in global.asax in Application_Start:



FilterConfig.RegisterGlobalFilters(GlobalFilters.F ilters);


And FilterConfig:



public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleAndLogErrorAttribute());
}
}


With the above you do not need boilerplate try/catch/log code for all your MVC Controllers that are Direct Method handlers. However, if you want to, you can still do that, assuming that when you catch unexpected exceptions you still return this.Direct(false, [error message]) after you have logged the exception or done whatever it is you need to.

Hope that helps.

Question: while this works for my needs, can this be improved? Are there some conditions/scenarios missed out, in particular for Ext.NET based AJAX request handling?

RaphaelSaldanha
Jan 06, 2015, 1:10 PM
Anup, thanks for sharing.

Daniil
Jan 07, 2015, 3:22 PM
Hi Anup,

Thank you for sharing!


while this works for my needs, can this be improved?

There is no limit to perfection... Though I don't see any room for improvements in this case:)


Are there some conditions/scenarios missed out, in particular for Ext.NET based AJAX request handling?

At first glance I don't see anything missed. Though, it doesn't guarantee nothing is missed:)

Do you mind I move this thread to the Examples and Extras forum?

anup
Jan 07, 2015, 3:32 PM
You read my mind! I was going to ask if you could move it to Example forum if you think it is okay... yes, please go ahead!

Thanks!

RaphaelSaldanha
Jan 07, 2015, 5:50 PM
DirectResult (RequestManager.IsAjaxRequest)



<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<script type="text/javascript">
var Button1Click = function () {
Ext.net.DirectMethod.request({
url: Ext.net.ResourceMgr.resolveUrl("~/Example/Action001"),
success: function () {
Ext.Msg.alert("Info", "Success");
},
failure: function (response) {
Ext.Msg.alert("Info", response);
}
});
}
var Button2Click = function () {
Ext.net.DirectMethod.request({
url: Ext.net.ResourceMgr.resolveUrl("~/Example/Action002"),
success: function () {
Ext.Msg.alert("Info", "Success");
},
failure: function (response) {
Ext.Msg.alert("Info", response);
}
});
}

</script>
</head>
<body>
<ext:ResourceManager runat="server" ScriptMode="Debug" />
<ext:Button Text="Button 1" runat="server">
<Listeners>
<Click Handler="Button1Click();" />
</Listeners>
</ext:Button>
<ext:Button Text="Button 2" runat="server">
<Listeners>
<Click Handler="Button2Click();" />
</Listeners>
</ext:Button>
</body>
</html>




namespace SandBox.Controllers
{
public class ExampleController : System.Web.Mvc.Controller
{
public ActionResult Index()
{
return View();
}

public AjaxResult Action001()
{
throw new Exception();
}

public AjaxResult Action002()
{
throw new Exception();
}
}
}


It would be possible to capture all failures by using ResourceManager's AjaxRequestException, as shown below:



<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<script type="text/javascript">
var Button1Click = function () {
Ext.net.DirectMethod.request({
cancelFailureWarning: true,
url: Ext.net.ResourceMgr.resolveUrl("~/Example/Action001"),
success: function () {
Ext.Msg.alert("Info", "Success");
}
});
}
var Button2Click = function () {
Ext.net.DirectMethod.request({
cancelFailureWarning: true,
url: Ext.net.ResourceMgr.resolveUrl("~/Example/Action002"),
success: function () {
Ext.Msg.alert("Info", "Success");
}
});
}

var ProcessAjaxRequestException = function (response, result, el, eventType, action, extraParams, o) {
if (!Ext.isEmpty(result.errorMessage)) {
Ext.Msg.alert("Info", result.errorMessage);
}
}
</script>
</head>
<body>
<ext:ResourceManager runat="server" ScriptMode="Debug">
<Listeners>
<AjaxRequestException Fn="ProcessAjaxRequestException" />
</Listeners>
</ext:ResourceManager>
<ext:Button Text="Button 1" runat="server">
<Listeners>
<Click Handler="Button1Click();" />
</Listeners>
</ext:Button>
<ext:Button Text="Button 2" runat="server">
<Listeners>
<Click Handler="Button2Click();" />
</Listeners>
</ext:Button>
</body>
</html>


JsonResult (filterContext.HttpContext.Request.IsAjaxRequest() )



<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<script type="text/javascript">
var ProcessStoreException = function (proxy, response, operation) {
if (!Ext.isEmpty(response.responseText)) {
var decodedMessage = Ext.decode(response.responseText);

if (!decodedMessage.success) {
Ext.Msg.alert("Info", decodedMessage.error);
}
}
}
</script>
</head>
<body>
<ext:ResourceManager runat="server" ScriptMode="Debug" />

<ext:GridPanel runat="server" Title="Records" Frame="false" Width="500" Height="500">
<Store>
<ext:Store AutoLoad="true" ID="_str" ShowWarningOnFailure="false" runat="server">
<Proxy>
<ext:AjaxProxy Url="~/Example/LoadFakeRecords/">
<ActionMethods Read="POST" />
<Reader>
<ext:JsonReader RootProperty="data" />
</Reader>
</ext:AjaxProxy>
</Proxy>
<Model>
<ext:Model IDProperty="ID" runat="server">
<Fields>
<ext:ModelField Name="ID" Type="String" />
<ext:ModelField Name="Name" Type="String" />
</Fields>
</ext:Model>
</Model>
<Listeners>
<Exception Fn="ProcessStoreException" />
</Listeners>
</ext:Store>
</Store>
<ColumnModel runat="server">
<Columns>
<ext:Column Text="ID" DataIndex="ID" runat="server" />
<ext:Column Text="Name" DataIndex="Name" runat="server" />
</Columns>
</ColumnModel>
</ext:GridPanel>
</body>
</html>




namespace SandBox.Controllers
{
public class ExampleController : System.Web.Mvc.Controller
{
public ActionResult Index()
{
return View();
}

public StoreResult LoadFakeRecords()
{
throw new Exception();
}
}
}


it's also possible to use ResourceManager's AjaxRequestException, as shown below:



<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<script type="text/javascript">
var ProcessAjaxRequestException = function (response, result, el, eventType, action, extraParams, o) {
if (!Ext.isEmpty(response.responseText)) {
var decodedMessage = Ext.decode(response.responseText);

if (!decodedMessage.success) {
Ext.Msg.alert("Info", decodedMessage.error);
}
}
}
</script>
</head>
<body>
<ext:ResourceManager runat="server" ScriptMode="Debug">
<Listeners>
<AjaxRequestException Fn="ProcessAjaxRequestException" />
</Listeners>
</ext:ResourceManager>
<ext:GridPanel runat="server" Title="Records" Frame="false" Width="500" Height="500">
<Store>
<ext:Store AutoLoad="true" ID="_str" ShowWarningOnFailure="false" runat="server">
<Proxy>
<ext:AjaxProxy Url="~/Example/LoadFakeRecords/">
<ActionMethods Read="POST" />
<Reader>
<ext:JsonReader RootProperty="data" />
</Reader>
</ext:AjaxProxy>
</Proxy>
<Model>
<ext:Model IDProperty="ID" runat="server">
<Fields>
<ext:ModelField Name="ID" Type="String" />
<ext:ModelField Name="Name" Type="String" />
</Fields>
</ext:Model>
</Model>
</ext:Store>
</Store>
<ColumnModel runat="server">
<Columns>
<ext:Column Text="ID" DataIndex="ID" runat="server" />
<ext:Column Text="Name" DataIndex="Name" runat="server" />
</Columns>
</ColumnModel>
</ext:GridPanel>
</body>
</html>



At this point, failure on both scenarios can be intercepted in ResourceManager's AjaxRequestException, but the code is different for each scenario.

So, we might be able to standardize the return of each scenario, and by doing so, we may catch any unknow ajax failure in ResourceManager's AjaxRequestException.

anup
Jan 07, 2015, 7:29 PM
Thanks for those examples, but I wasn't looking for doing global handling on the client side, which I have covered. I was particularly interested in handling exceptions in the controller so that ASP.NET MVC didn't redirect to the normal error view (assuming that is set up) or show the default error page etc. Instead, I wanted to intercept it for Ext.NET ajax requests so that I could do common stuff on the server, such as logging the exception, while telling ASP.NET MVC that the exception was handled so a more graceful DirectResult/JSON could be returned to the client side which can then continue handling failures as normal.

RaphaelSaldanha
Jan 07, 2015, 8:23 PM
My primary aim was to provide examples of its utilization, since some users may get confused.

But after i was done i realized that we could handle it "globally".

Once again, thank you for sharing, It may be very useful.