Custom error filter for json requests in ASP.NET Core 1.0

Errors while doing a json request

They suck! You are waiting on your webpage for a XHR request to complete, but without knowing it failed.

2016-07-08 15_56_30

Angular has some functionality that you can catch any result code outside 200 – 299, but than you’ll give a general error to the user.

What if you could really inform the user what went wrong…

Creating an exception filter in APS.NET Core 1.0

With an exception filter you can capture any given exception that occurs during the request.

filter-pipeline-2

Different filter types run at different points within the pipeline. Some filters, like authorization filters, only run before the next stage in the pipeline, and take no action afterward. Other filters, like action filters, can execute both before and after other parts of the pipeline execute, as shown below. Source: https://docs.asp.net/en/latest/mvc/controllers/filters.html#how-do-filters-work

Exception filters handle unhandled exceptions, including those that occur during controller creation and model binding. They are only called when an exception occurs in the pipeline. They can provide a single location to implement common error handling policies within an app.

I created an exception filter that looks at the route that is used. Only if the route is the json route (see the startup.cs), than this filter kicks in.
It creates a new json object with the name of the MVC Controller, Action and exception message. If an inner exception exists, this is added as well.

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly ProjectSettings _projectSettings;

    public CustomExceptionFilterAttribute(IOptions<ProjectSettings> projectSettings)
    {
        _projectSettings = projectSettings.Value;
    }

    public override void OnException(ExceptionContext context)
    {
        // If detailed exceptions should be shown
        if (_projectSettings.DetailedExcpetions) return;

        // Only catch if this is a jsonRoute
        if (context.RouteData.Routers
            .OfType<Route>()
            .Any(route => route.Name != "jsonRoute")) return;

        const bool coreError = true;
        var mvcController = context.ActionDescriptor.RouteValues["controller"];
        var mvcAction = context.ActionDescriptor.RouteValues["action"];
        var errorMessage = $"{context.Exception.Message} {context.Exception.InnerException?.Message}";
            
        var result = new JsonResult(new { coreError, mvcController, mvcAction, errorMessage });

        context.Result = new ObjectResult(result)
        {
            StatusCode = (int)HttpStatusCode.InternalServerError
        };
    }
}

The result would be something like this:
2016-07-08 16_37_16

My startup.cs has the route names described as follows

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "htmlRoute",
        template: "{controller}/{action}.html",
        defaults: new {controller = "Home", action = "Index"});

    routes.MapRoute(
        name: "jsonRoute",
        template: "{controller}/{action}.json",
        defaults: new {controller = "Home", action = "Index"});
});

In the startup.cs the filter is also added under the AddMvc method

services.AddMvc(options =>
{
    options.Filters.Add(typeof (CustomExceptionFilterAttribute));
})

Displaying the error with some Angular magic

The end result will look like:

2016-07-06 15_30_13

In order to achive this, every http request has to be checked by Angular. This is were an interceptor comes in handy.
Upon a responseError, a check is done if all necessary objects are available to construct a message of.
It then uses a dialogService to display the message on the webpage:

startApp.config([
    "$httpProvider", function($httpProvider) {
        //disable IE ajax request caching
        if (!$httpProvider.defaults.headers.get) {
            $httpProvider.defaults.headers.get = {};
        }
        $httpProvider.defaults.headers.get["If-Modified-Since"] = "Mon, 26 Jul 1997 05:00:00 GMT";
        $httpProvider.defaults.headers.get["Cache-Control"] = "no-cache";
        $httpProvider.defaults.headers.get["Pragma"] = "no-cache";

        $httpProvider.interceptors.push(function($location, $injector, $q) {
            return {
                'request': function(config) {
                    return config;
                },
                "requestError": function(rejection) {
                    return $q.reject(rejection);
                },
                "response": function(response) {
                    return response;
                },
                "responseError": function(rejection) {
                    var dialogService = $injector.get("dialogService");
                    var message = "Er is een fout opgetreden.";

                    if (rejection.data &amp;&amp; rejection.data.value) {
                        var value = rejection.data.value;
                        if (value.coreError) {
                            message = value.mvcController + " - " + value.mvcAction + ": " + value.errorMessage;
                        }
                    }

                    dialogService.displayNotification("danger", message, 10);

                    return $q.reject(rejection);
                }
            };
        });
    }
]);

The dialogService looks as follows.
It uses the alert directive from AngularStrap to display the message inside the DOM.

function initDialogService() {
	"use strict";

	var module = angular.module("startApp");
	module.service("dialogService", dialogService);

	var notificationId = 0;

	function dialogService($rootScope, $aside, $alert) {
		var service = {};

		$rootScope.notificationMessages = [];

		service.displayNotification = function(type, message, delay) {
		var notification = createNotification(type, message);
		$rootScope.notificationMessages.push(notification);

		$alert({
                templateUrl: "alert.tpl",
                content: message,
                animation: "am-slide-top",
				type: type,
				show: true,
				duration: delay
			});
		};

		service.closeNotification = function(dialogId) {
			$rootScope.$broadcast("closenotification", dialogId);
		};

		return service;
	}

	function createNotification(type, message) {
		return {
			id: ++notificationId,
			type: type,
			message: message,
			date: new Date()
		};
	}
}
initDialogService();

The alert.tpl is included in the main index.cshtml file making sure it is available at any time at any place:

<script type="text/ng-template" id="alert.tpl">
    <div class="alert top" ng-class="[type ? 'alert-' + type : null]">
        <button type="button" class="close" ng-click="$hide()">
            <i class="fa fa-times-circle fa-1point5x"></i>
        </button>
        <span ng-bind-html="content"></span>
    </div>
</script>

One thought on “Custom error filter for json requests in ASP.NET Core 1.0

  • Под видеонаблюдение мы имеем ввиду проводные или беспроводные охранные системы для дома,
    которые свой сигнал тревоги передают не на милицейский пульт, а при помощи встроенного GSM модуля,
    отправляют на ваш мобильный телефон СМС или делают вам звонок.

Comments are closed.