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.