Custom Errors in ASP.NET Core and MVC 6

 

        Introduction:

 

                 One of the famous feature of classic ASP.NET is custom errors which provides information about custom error messages which may be useful for diagnosing issues. ASP.NET Core also provides some middlewares to show diagnostic information and generate error page responses. You can read more about Diagnotics in ASP.NET Core at here.  I am not going to repeat the things that has been discussed in that post here. Rather, my intention will be to show you how to do custom errors like stuffs that we are used to do in classic ASP.NET.  

 

        Description:

 

                    Let's create an ASP.NET Core web application (note that I am using beta4 at the time of writing). The default template include these diagnostics lines, 

 

01if (env.IsEnvironment("Development"))
02{
03    // omitted for brevity
04    app.UseErrorPage(ErrorPageOptions.ShowAll);
05    // omitted for brevity
06}
07else
08{
09    // Add Error handling middleware which catches all application specific errors and
10    // sends the request to the following path or controller action.
11    app.UseErrorHandler("/Home/Error");
12}

 

                    In classic ASP.NET, we use customErrors.mode in web.config to change the behaviour of response error pages so that our application will not leak security information. In ASP.NET Core, we can easily and explicitly opt-in or opt-out the error pages and its details about exception, source code, cookies, query-string, headers, etc., using UseErrorPage extension method. The default ASP.NET Core template that comes with Visual Studio 2015 RC only opt-in this feature if there is an environment variable called ASPNET_ENV and its value set to Development(during development Visual Studio automatically create this variable for us) which makes it harder for developers to accidently opt-in this feature in production.  There is another extension method called UseErrorHandler. We can use this method in production to show generic error page or execute middleware if there is an unhandled exception thrown by application. This method is looks akin to classic ASP.NET's customErrors.defaultRedirect(but no redirect happen in ASP.NET Core ). We can pass a path(as shown above) or generic middlware(s) pipeline. For example(taken from),

 

01//Configure the error handler to show an error page.
02 app.UseErrorHandler(errorApp =>
03 {
04    // Normally you'd use MVC or similar to render a nice page.
05    errorApp.Run(async context =>
06   {
07         context.Response.StatusCode = 500;
08         context.Response.ContentType = "text/html";
09         await context.Response.WriteAsync("<html><body>\r\n");
10         await context.Response.WriteAsync("We're sorry, we encountered an un-expected issue with your application.<br>\r\n");
11 
12         var error = context.GetFeature<IErrorHandlerFeature>();
13         if (error != null)
14         {
15            // This error would not normally be exposed to the client
16            await context.Response.WriteAsync("<br>Error1: " + HtmlEncoder.Default.HtmlEncode(error.Error.Message) + "<br>\r\n");
17         }
18         await context.Response.WriteAsync("<br><a href=\"/\">Home</a><br>\r\n");
19         await context.Response.WriteAsync("</body></html>\r\n");
20         await context.Response.WriteAsync(new string(' ', 512)); // Padding for IE
21     });
22 });       

 

                    There is another middleware called StatusCodePagesMiddleware, which we can use to return a custom error-page or execute a middleware to return response or redirect if the response status code is between 400 and 600. Generally, these status codes are used to indicate client(for 4xx) and/or server(for 5xx) error. The StatusCodePagesMiddleware can be very useful if we want to return a generic error response(or redirect) if some other middleware(e.g. mvc. web-api or authentication) mark the request as invalid using response staus-code(4xx or 5xx). Here is an example(from here),

 

1app.UseStatusCodePages(); // There is a default response but any of the following can be used to change the behavior.
2 
3// app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
4// app.UseStatusCodePages("text/plain", "Response, status code: {0}");
5// app.UseStatusCodePagesWithRedirects("~/errors/{0}"); // PathBase relative
6// app.UseStatusCodePagesWithRedirects("/base/errors/{0}"); // Absolute
7// app.UseStatusCodePages(builder => builder.UseWelcomePage());
8// app.UseStatusCodePagesWithReExecute("/errors/{0}");

 

                    In the above code, we also have extension methods for re-executing or redirecting the request(in classic ASP.NET world, we have customErrors.redirectMode=ResponseRedirect | ResponseRewrite). The above code will return a default response if the request is marked as invalid(means response status-code is set between 400 and 600) by some middleware. In some cases, some middleware may be willing to set both the status code between 400 and 600 as well as response at runtime. They can disable the status code page middleware by setting IStatusCodePagesFeature.Enabled to false. Here is an example,

 

1var statusCodePagesFeature = context.GetFeature<IStatusCodePagesFeature>();
2if (statusCodePagesFeature != null)
3{
4      statusCodePagesFeature.Enabled = false;
5}

 

                    I think there is something missing in this feature. In classic ASP.NET world we have customErrors > error.statusCode and customErrors > error.redirect, which only redirect the user if a specific status code matches.  We can very easily add/enable this feature using,

 

001public static class MyStatusCodePagesExtensions
002{
003    /// <summary>
004    /// Adds a StatusCodePages middleware with the given options that checks for responses with status codes
005    /// between 400 and 599 that do not have a body.
006    /// </summary>
007    /// <param name="app"></param>
008    /// <param name="options"></param>
009    /// <returns></returns>
010    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options, int statusCode)
011    {
012        return app.UseMiddleware<MyStatusCodePagesMiddleware>(options, statusCode);
013    }
014 
015    /// <summary>
016    /// Adds a StatusCodePages middleware with a default response handler that checks for responses with status codes
017    /// between 400 and 599 that do not have a body.
018    /// </summary>
019    /// <param name="app"></param>
020    /// <returns></returns>
021    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, int statusCode)
022    {
023        return UseStatusCodePages(app, new StatusCodePagesOptions(), statusCode);
024    }
025 
026    /// <summary>
027    /// Adds a StatusCodePages middleware with the specified handler that checks for responses with status codes
028    /// between 400 and 599 that do not have a body.
029    /// </summary>
030    /// <param name="app"></param>
031    /// <param name="handler"></param>
032    /// <returns></returns>
033    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Func<StatusCodeContext, Task> handler, int statusCode)
034    {
035        return UseStatusCodePages(app, new StatusCodePagesOptions() { HandleAsync = handler }, statusCode);
036    }
037 
038    /// <summary>
039    /// Adds a StatusCodePages middleware with the specified response body to send. This may include a '{0}' placeholder for the status code.
040    /// The middleware checks for responses with status codes between 400 and 599 that do not have a body.
041    /// </summary>
042    /// <param name="app"></param>
043    /// <param name="contentType"></param>
044    /// <param name="bodyFormat"></param>
045    /// <returns></returns>
046    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, string contentType, string bodyFormat, int statusCode)
047    {
048        return UseStatusCodePages(app, context =>
049        {
050            var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode);
051            return context.HttpContext.Response.SendAsync(body, contentType);
052        }, statusCode);
053    }
054 
055    /// <summary>
056    /// Adds a StatusCodePages middleware to the pipeine. Specifies that responses should be handled by redirecting
057    /// with the given location URL template. This may include a '{0}' placeholder for the status code. URLs starting
058    /// with '~' will have PathBase prepended, where any other URL will be used as is.
059    /// </summary>
060    /// <param name="app"></param>
061    /// <param name="locationFormat"></param>
062    /// <returns></returns>
063    public static IApplicationBuilder UseStatusCodePagesWithRedirects(this IApplicationBuilder app, string locationFormat, int statusCode)
064    {
065        if (locationFormat.StartsWith("~"))
066        {
067            locationFormat = locationFormat.Substring(1);
068            return UseStatusCodePages(app, context =>
069            {
070                var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
071                context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location);
072                return Task.FromResult(0);
073            }, statusCode);
074        }
075        else
076        {
077            return UseStatusCodePages(app, context =>
078            {
079                var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
080                context.HttpContext.Response.Redirect(location);
081                return Task.FromResult(0);
082            }, statusCode);
083        }
084    }
085 
086    /// <summary>
087    /// Adds a StatusCodePages middleware to the pipeline with the specified alternate middleware pipeline to execute
088    /// to generate the response body.
089    /// </summary>
090    /// <param name="app"></param>
091    /// <param name="configuration"></param>
092    /// <returns></returns>
093    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Action<IApplicationBuilder> configuration, int statusCode)
094    {
095        var builder = app.New();
096        configuration(builder);
097        var tangent = builder.Build();
098        return UseStatusCodePages(app, context => tangent(context.HttpContext), statusCode);
099    }
100 
101    /// <summary>
102    /// Adds a StatusCodePages middleware to the pipeline. Specifies that the response body should be generated by
103    /// re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code.
104    /// </summary>
105    /// <param name="app"></param>
106    /// <param name="pathFormat"></param>
107    /// <returns></returns>
108    public static IApplicationBuilder UseStatusCodePagesWithReExecute(this IApplicationBuilder app, string pathFormat, int statusCode)
109    {
110        return UseStatusCodePages(app, async context =>
111        {
112            var newPath = new PathString(string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
113 
114            var originalPath = context.HttpContext.Request.Path;
115            // Store the original paths so the app can check it.
116            context.HttpContext.SetFeature<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
117            {
118                OriginalPathBase = context.HttpContext.Request.PathBase.Value,
119                OriginalPath = originalPath.Value,
120            });
121 
122            context.HttpContext.Request.Path = newPath;
123            try
124            {
125                await context.Next(context.HttpContext);
126            }
127            finally
128            {
129                context.HttpContext.Request.Path = originalPath;
130                context.HttpContext.SetFeature<IStatusCodeReExecuteFeature>(null);
131            }
132        }, statusCode);
133    }
134}
135 
136public class MyStatusCodePagesMiddleware
137{
138    private readonly RequestDelegate _next;
139    private readonly StatusCodePagesOptions _options;
140    private readonly int _statusCode;
141 
142    public MyStatusCodePagesMiddleware(RequestDelegate next, StatusCodePagesOptions options, int statusCode)
143    {
144        _next = next;
145        _options = options;
146        if (_options.HandleAsync == null)
147        {
148            throw new ArgumentException("Missing options.HandleAsync implementation.");
149        }
150        _statusCode = statusCode;
151    }
152 
153    public async Task Invoke(HttpContext context)
154    {
155        var statusCodeFeature = new StatusCodePagesFeature();
156        context.SetFeature<IStatusCodePagesFeature>(statusCodeFeature);
157 
158        await _next(context);
159 
160        if (!statusCodeFeature.Enabled)
161        {
162            // Check if the feature is still available because other middleware (such as a web API written in MVC) could
163            // have disabled the feature to prevent HTML status code responses from showing up to an API client.
164            return;
165        }
166 
167        // Do nothing if a response body has already been provided.
168        if (context.Response.HeadersSent
169            || context.Response.StatusCode < 400
170            || context.Response.StatusCode >= 600
171            || context.Response.StatusCode != _statusCode // additional check
172            || context.Response.ContentLength.HasValue
173            || !string.IsNullOrEmpty(context.Response.ContentType))
174        {
175            return;
176        }
177 
178        var statusCodeContext = new StatusCodeContext(context, _options, _next);
179        await _options.HandleAsync(statusCodeContext);
180    }
181}

 

                         

                    In the above code, we have just added additional overloads of UseStatusCodePagesXXX which adds an additional parameter called statusCode. We can now easily do what we have used to do in classic ASP.NET. You can test this by adding these lines on Startup class,

 

1app.UseStatusCodePagesWithRedirects("~/400.html", 400);
2app.UseStatusCodePagesWithRedirects("~/500.html", 500);

 

        Summary:

 

                    Diagnostics feature in ASP.NET Core is much more improved. In this article, I showed you how we can do custom errors like stuffs in ASP.NET Core that we are doing with classic ASP.NET.

3 Comments

  • I don't get why this is better than handling errors globally in Application.OnError(Exception e) {}

  • As we know that ASP.NET is an open source server-side language, which designed to make web development for design superb web pages. it also known as custom errors because it gives custom errors messages service which useful for diagnosing issues. If you want to do .NET training, then I suggest you for best DOT NET development courses in Jaipur.

    Source:http://www.sagacademy.com/dotnet-development-training-jaipur

  • As we know that ASP.NET is an open source server-side language, which designed to make web development for design superb web pages. it also known as custom errors because it gives custom errors messages service which useful for diagnosing issues. If you want to do .NET training, then I suggest you for best DOT NET development courses in Jaipur.

Comments have been disabled for this content.