Attention: We have retired the ASP.NET Community Blogs. Learn more >

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, 

 

01 if (env.IsEnvironment("Development"))
02 {
03     // omitted for brevity
04     app.UseErrorPage(ErrorPageOptions.ShowAll);
05     // omitted for brevity
06 }
07 else
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),

 

1 app.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,

 

1 var statusCodePagesFeature = context.GetFeature<IStatusCodePagesFeature>();
2 if (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,

 

001 public 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  
136 public 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,

 

1 app.UseStatusCodePagesWithRedirects("~/400.html", 400);
2 app.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.