我们在使用某些系统的时候,如果访问了一个不存在的地址怎么办,比如http://52abp. com/airport/fly;或者访问的地址路由和操作方法虽然存在,但是地址参数id不存在,比如https://www.52abp.com/BlogDetails/5这个信息是存在的,但是更改访问https://www.52abp.com/BlogDetails/100时,则提示博客内容不存在。
在我们的系统中要如何处理呢?
本章主要向读者介绍如下内容。
- ASP.NET Core MVC中的两种404错误。
- 如何在ASP.NET Core MVC中处理404 Not Found错误。
HTTP状态码是用以表示网页服务器HTTP响应状态的3位数字代码。状态码的第一个数字代表了响应的5种状态之一,这里我们只介绍涉及的4××和5××系列,之前讲解过状态码3××和2××。
4××系列表示请求错误,代表了客户端可能发生了错误,从而妨碍了服务器的处理。常见的有401状态码、403状态码和404状态码。
- 401状态码:请求要求身份验证。对于需要登录的网页,服务器可能返回此响应。
- 403状态码:服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。
- 404状态码:请求失败,请求希望得到的资源在服务器上未发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源,因为某些内部的配置机制问题,访问内容已不可用,而且没有任何可以跳转的地址。404状态码被广泛应用于服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。
5××系列表示服务器在处理请求的过程中有错误或者异常状态发生,也可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。常见的有500状态码和503状态码。
- 500状态码:服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器的程序码出错时出现。
- 503状态码:由于临时的服务器维护或者过载,因此服务器当前无法处理请求。通常这是暂时状态,一段时间后会恢复。
HTTP状态码是服务器和客户端之间交流信息的语言。通过查看网站日志的HTTP状态码,我们可以清楚地查看搜索引擎在网站的爬取情况。
19.1.1 ASP.NET Core中的404错误404错误信息有两种,我们在之前已经提及了。首先了解第一种:找不到指定ID的信息。当无法通过指定的ID找到学生、产品和客户等信息的时候产生404错误,可以参考HomeController中的Details()的方法,代码如下。
var student = _studentRepository.GetStudentById(id);
//判断学生信息是否存在
if(student == null)
{
Response.StatusCode = 404;
return View("StudentNotFound",id);
}
//实例化HomeDetailsViewModel并存储Student详细信息和PageTitle
HomeDetailsViewModel homeDetailsViewModel = new HomeDetailsViewModel()
{
Student = student,
PageTitle = "学生详情"
};
//将HomeDetailsViewModel对象传递给View()方法
return View(homeDetailsViewModel);
可以通过传递一个ID为99的值来调用HomeController中的Details()方法:http://localhost:13380/home/details/99,查询不到该学生的信息,然后跳转到StudentNotFound视图中。
19.1.2 404错误信息的视图代码在Views/Home文件夹中创建一个名为StudentNotFound.cshtml的视图文件,我们使用Bootstrap 4的样式来优化视图,代码如下。
@model int
@{
ViewBag.Title = "404错误";
}
<div class="alert alert-danger mt-1 mb-1">
<h4>404 Not Found错误 :</h4>
<hr/>
<h5>
查询不到学生ID为 @Model的信息。
</h5>
</div>
<a asp-controller="home" asp-action="index" class="btn btn-outline-success"
style="width:auto">单击此处查看学生信息列表</a>
在这种情况下,我们知道用户正在尝试转到学生详情视图页面,但因为提供的ID值无效,所以我们需要返回一个带有提示消息的自定义错误页面,提示用户找不到ID以及可以查看学生信息列表的链接,效果如图19.1所示。
图19.1
第二种:请求的URL和路由不匹配。请参考http://localhost:13380/market/food,它也会触发404错误异常信息。在这种情况下,我们无法知道用户到底在访问什么页面,因此无法显示自定义错误页面。我们通常都会返回一个统一的错误页面。
19.2 统一处理ASP.NET Core中的404错误在本节中,我们将学习如何在ASP.NET Core中统一处理404错误,即Page Not Found错误。在此过程中,我们将学习以下3个中间件,这些组件的作用是处理ASP.NET Core中的状态码页。
- UseStatusCodePages()。
- UseStatusCodePagesWithRedirects()。
- UseStatusCodePagesWithReExecute()。
在ASP.NET Core中,有两种类型的404错误可能发生。
- 找不到指定ID的资源信息。关于如何处理这种类型的404错误,我们在前面的章节已经介绍了——制作自定义的错误页面。
- 请求的URL和路由不匹配。在本节中,我们将学习如何以统一的方式处理此类404错误。
以下是Startup类的Configure()方法的代码。读者可能已经知道,这个Configure() 方法用于配置ASP.NET Core应用程序的HTTP请求处理管道。
public void Configure(IApplicationBuilder app,IWebHostEnvironment env)
{
//如果环境是Development serve Developer Exception Page
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//否则显示用户友好的错误页面
else if(env.IsStaging() || env.IsProduction() || env.IsEnvironment("UAT"))
{
app.UseExceptionHandler("/Error");
}
//使用纯静态文件支持的中间件,而不使用带有终端的中间件
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name:"default",
pattern:"{controller=Home}/{action=Index}/{id?}");
});
}
目前,我们在此HTTP请求处理管道中没有配置任何处理404错误的内容。因此,如果导航到http://localhost:13380/market/food,我们会看到图19.2所示的默认404错误页面。这是因为URL/market/food与应用程序中的所有路由都不匹配,从而引发了错误。
图19.2
19.3 处理失败的HTTP状态码为了处理失败的HTTP状态码,比如404,我们可以使用以下3个内置的ASP.NET Core中间件。
- UseStatusCodePages()。
- UseStatusCodePagesWithRedirects()。
- UseStatusCodePagesWithReExecute()。
我认为这是3个状态码中间件中最不实用的,因为我们很少在生产中使用它。要在应用程序中使用它并查看其可以执行的操作,请将其插入HTTP处理管道,代码如下。
public void Configure(IApplicationBuilder app,IWebHostEnvironment env)
{
//如果环境是Development serve Developer Exception Page
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{ //用于处理错误异常
app.UseStatusCodePages();
}
//使用纯静态文件支持的中间件,而不使用带有终端的中间件
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name:"default",
pattern:"{controller=Home}/{action=Index}/{id?}");
});
}
"IIS Express":{
"commandName":"IISExpress",
"launchBrowser":true,
"environmentVariables":{
"ASPNETCORE_ENVIRONMENT":"Production",
"MyKey":" launchsettings.json中Mykey的值"
}
},
因为添加了UseStatusCodePages()中间件,所以如果我们浏览http://localhost:13380/market/food,则会返回如图19.3所示的简单文本响应。
图19.3
请注意,当环境变量不是Development的时候,才会触发404错误,毕竟这是给用户查看的,所以要将launchSettings中的环境变量值修改为Staging,否则无法触发该异常。
19.3.2 UseStatusCodePagesWithRedirects中间件在生产中,我们希望拦截这些访问失败的HTTP状态码,并返回自定义错误视图。为此,我们可以使用UseStatusCodePagesWithRedirects()中间件或UseStatusCodePagesWith-ReExecute()中间件,代码如下。
public void Configure(IApplicationBuilder app,IWebHostEnvironment env)
{
//如果环境是Development serve Developer Exception Page
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseStatusCodePagesWithRedirects("/Error/{0}");
}
//使用纯静态文件支持的中间件,而不使用带有终端的中间件
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name:"default",
pattern:"{controller=Home}/{action=Index}/{id?}");
});
}
我们将下面的代码添加到Configure()方法中,如果出现404错误,则会将用户重定向到/Error/404。这里采用了占位符 {0} ,它会自动接收HTTP中的状态码。
app.UseStatusCodePagesWithRedirects("/Error/{0}");
因为使用了UseStatusCodePagesWithRedirects()中间件,所以要让它统一显示错误信息,我们需要添加对应的控制器和视图代码,代码如下。
public class ErrorController:Controller
{
//如果状态码为404,则路径将变为Error/404
[Route("Error/{statusCode}")]
public IActionResult HttpStatusCodeHandler(int statusCode)
{
switch(statusCode)
{
case 404:
ViewBag.ErrorMessage = "抱歉,读者访问的页面不存在";
break;
}
return View("NotFound");
}
}
在Views/Error文件夹中创建一个NotFound.cshtml视图文件,代码如下。
@{
ViewBag.Title = "页面不存在";
}
<h1>@ViewBag.ErrorMessage</h1>
<a asp-action="index" asp-controller="home">
单击此处返回首页
</a>
此时,如果进入http://localhost:13380/market/food,我们会看到页面已经被导航到了NotFound.cshtml页面,显示自定义404错误信息,如图19.4所示。
图19.4
到现在为止我们还有一个中间件没有讲,那就是UseStatusCodePagesWithReExecute()。
我们将Configure()方法中的app.UseStatusCodePagesWithRedirects("/Error/{0}");替换为app.UseStatusCodePagesWithReExecute("/Error/{0}");。重新运行应用程序并导航到http://localhost:13380/market/food,我们看到在NotFound.cshtml文件中同样触发了相同的自定义404错误信息。
在这一点上我们想到的一个显而易见的问题是,这两个中间件之间的区别是什么,我们应该使用哪一个呢?接下来我们将对比它们的不同。
19.4 UseStatusCodePagesWithRedirects与UseStatusCodePagesWithReExecute我们将讨论UseStatusCodePagesWithRedirects()和UseStatusCodePagesWithReExecute()中间件之间的区别。
从最终呈现到页面上的角度来看,无论读者使用哪种中间件,产生的结果都没有区别。我们在两种情况下都看到了指定的自定义错误视图。
19.4.1 UseStatusCodePagesWithRedirects中间件说明目前在Startup类中注册了UseStatusCodePagesWithReExecute()中间件,代码如下。
app.UseStatusCodePagesWithRedirects("/Error/{0}");
通过访问一个不存在的控制器与操作方法,如http://localhost:13380/market/food,发起请求时,由于此URL与我们的应用程序中的任何路由都不匹配,因此会引发404错误。
这是因为UseStatusCodePagesWithRedirects()中间件会拦截404状态码,顾名思义,它表示发出重定向到指定的错误路径中(在本例中路径为/Error/404)。
19.4.2 UseStatusCodePagesWithRedirects请求处理流程使用UseStatusCodePagesWithRedirects()中间件,当向http://localhost:13380/market/food发出请求时会触发404状态码,流程如下。
- StatusCodePagesWithRedirects()中间件拦截此请求,并将其更改为302,将其指向错误路径(/Error/404)。
- 302状态码表示所请求资源的URL已被暂时更改,在我们的示例中,它被更改为/Error/404。因此,它会发出另一个GET请求以满足重定向的请求。
- 由于发出了重定向,因此地址栏中的URL也从/market/food更改为/Error/404。
- 请求会经过HTTP管道并由MVC中间件处理,最终返回状态码为200,然后导航到NotFound视图中,这意味着请求已成功完成。
- 对整个请求流程中的浏览器而言,没有404错误信息。
- 如果读者仔细观察此请求和响应流,就会发现在实际发生错误时返回成功状态码为200,这在语义上是不正确的。
运行结果如图19.5所示。
图19.5
19.4.3 使用UseStatusCodePagesWithReExecute请求处理流程如果要在应用程序中使用UseStatusCodePagesWithReExecute()中间件,则在Startup中将app.UseStatusCodePagesWithRedirects("/Error/{0}");替换为app. UseStatusCodePages WithReExecute("/Error/{0}")即可。
通过访问http://localhost:13380/market/food 发出请求时,同样会触发404状态码,流程如下。
- UseStatusCodePagesWithReExecute()中间件拦截404状态码并重新执行将其指向URL的管道,即/Error/404中。
- 整个请求流经HTTP管道并由MVC中间件处理,该中间件返回的NotFound视图HTML的状态码依然是200。
- 当响应流出到客户端时,它会通过UseStatusCodePagesWithReExecute()中间件使用HTML响应,将200状态码替换为原始的404状态码。
- 这个中间件重新执行管道应该正确的(404)状态码。它只返回自定义视图(NotFound)。
- 因为它只是重新执行管道而不发出重定向请求,所以我们还在地址栏中保留原始 http://localhost:13380/market/food
,它不会从/market/food更改为/Error/404。
运行结果如图19.6所示。
图19.6
如果读者正在使用UseStatusCodePagesWithReExecute()中间件,则还可以使用IStatusCodeReExecuteFeature接口在ErrorController中获取原始路径,代码如下。
public class ErrorController:Controller
{
//使用属性路由,如果状态码为404,则路径将变为Error/404
[Route("Error/{statusCode}")]
public IActionResult HttpStatusCodeHandler(int statusCode)
{
var statusCodeResult =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
switch(statusCode)
{
case 404:
ViewBag.ErrorMessage = "抱歉,读者访问的页面不存在";
ViewBag.Path = statusCodeResult.OriginalPath;
ViewBag.QS = statusCodeResult.OriginalQueryString;
break;
}
return View("NotFound");
}
}
代码说明如下。
- statusCodeResult.OriginalPath可以获取URL请求信息。
- statusCodeResult.OriginalQueryString可以获取查询字符串的搜索信息。
然后,在NotFound视图中进行自定义错误内容的优化,代码如下。
@{ViewBag.Title = "页面不存在";}
<h1>@ViewBag.ErrorMessage</h1>
<h1>@ViewBag.Path</h1>
<h1>@ViewBag.QS</h1>
<a asp-action="index" asp-controller="home"> 单击此处返回首页</a>
重新运行程序,在地址栏中输入http://localhost:13380/market/food/3?name=apple,得到的返回视图如图19.7所示。
图19.7
通过运行对比可以得知,UseStatusCodePagesWithReExecute()中间件不会改变请求地址,而UseStatusCodePagesWithRedirects()中间件则会跳转到ErrorController中进而改变请求地址。我们推荐采用UseStatusCodePagesWithReExecute()中间件,保留错误的URL信息,便于记录到日志文件中,这会在后面的章节中实现。
19.5 ASP.NET Core中的全局异常处理在本节中,我们将学习如何在ASP.NET Core中实现全局异常处理程序,并呈现任意非正常请求。
在以下Details()操作方法中,我们故意使用throw关键字抛出异常。
public ViewResult Details(int?id)
{
throw new Exception("在Details视图中抛出异常");
//其他代码
}
访问http://localhost:13380/Home/Details/2,结果如图19.8所示。
图19.8
可以看到返回的状态码是500,因为500错误是来自服务器的内部错误。
19.5.1 ASP.NET Core中的UseDeveloperExceptionPage中间件UseDeveloperExceptionPage()中间件是指当代码触发异常时,会进入开发者异常页面,代码如下。
public void Configure(IApplicationBuilder app,IHostingEnvironment env)
{
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//其他代码
}
从上面的代码中我们得知,已经将DeveloperExceptionPage()中间件配置到HTTP请求处理管道中 ,因此在开发环境中运行应用程序时,如果存在未处理的异常,则会触发如图19.9所示的开发人员异常页面。
图19.9
这里需要将launchSettings.json中的ASPNETCORE_ENVIRONMENT变量设置为Development。
因为我们添加了env.IsDevelopment()的判断,所以DeveloperExceptionPage()中间件只能在开发环境中触发,即环境变量为Development。比如,在Production这样的非开发环境中使用此页面存在安全风险,因为它包含可供攻击者使用的详细异常信息,而且此开发异常页面对最终用户也没有任何意义。
19.5.2 ASP.NET Core中的非开发环境异常信息现在我们需要在本地开发计算机上模拟生产环境,应修改应用程序中的环境变量。打开launchSettings.json文件将其中的ASPNETCORE_ENVIRONMENT变量设置为Production,表示当前开发环境已经为Production(生产环境),代码如下。
"ASPNETCORE_ENVIRONMENT":"Production"
在默认情况下,如果在生产等非开发环境中存在未处理的异常,则会看到如图19.10所示的默认页面。
图19.10
请注意,图19.10中除显示HTTP ERROR 500之外,没有显示任何其他信息。错误500表示服务器上出现错误,服务器不知道如何处理。
此默认页面对最终用户不是很有用。我们希望处理异常并将用户重定向到自定义错误视图,这更有意义。
19.5.3 ASP.NET Core中的异常处理ASP.NET Core中异常处理的步骤如下所示。
对于非开发环境,使用UseExceptionHandler()方法将异常处理中间件添加到请求处理管道。遇到异常的时候,异常处理中间件会跳转到ErrorController中,我们需要打开Startup类的Configure()方法,代码如下。
public void Configure(IApplicationBuilder app,IWebHostEnvironment env)
{
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
//其他代码
}
修改ErrorController代码,它会搜索异常详细信息并返回到指定的自定义错误视图。在生产中,不会在错误视图上显示异常详细信息。我们可以将它们记录到数据库表、文件和事件查看器等,以便开发人员查看它们,并在需要时提供代码修复。我们将在稍后的章节中讨论日志记录。
public class ErrorController:Controller
{
[Route("Error")]
public IActionResult Error()
{
//获取异常细节
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
ViewBag.ExceptionPath = exceptionHandlerPathFeature.Path;
ViewBag.ExceptionMessage = exceptionHandlerPathFeature.Error.Message;
ViewBag.StackTrace = exceptionHandlerPathFeature.Error.StackTrace;
return View("Error");
}
}
请注意,IExceptionHandlerPathFeature位于Microsoft.AspNetCore.Diagnostics命名空间中。
接下来实现错误视图,我们在Views/Error文件夹中添加一个Error.cshtml文件。
<h3>
程序请求时发生了一个内部错误,我们会反馈给团队,我们正在努力解决这个问题。
</h3>
<h5>请通过ltm@ddxc.org与我们取得联系</h5>
<hr />
<h3>错误详情:</h3>
<div class="alert alert-danger">
<h5>异常路径:</h5>
<hr />
<p>@ViewBag.ExceptionPath</p>
</div>
<div class="alert alert-danger">
<h5>异常信息:</h5>
<hr />
<p>@ViewBag.ExceptionMessage</p>
</div>
<div class="alert alert-danger">
<h5>异常堆栈跟踪:</h5>
<hr />
<p>@ViewBag.StackTrace</p>
</div>
请注意,当前系统的环境变量需要为Production或者非Development。
运行项目,访问http://localhost:13380/Home/Details/2,效果如图19.11所示。
图19.11
我们能获取到完整的错误信息,异常的路径、信息以及堆栈中错误的具体内容。
19.5.4 调整Edit()方法中的错误视图我们通过本机环境变量的配置,处理了404异常以及服务器内部报错的异常信息,两种触发方式的区别如下。
- 404错误触发是当访问不存在的地址或者ID没有对应的值时产生的。
- 500错误触发是当发生异常错误时产生的。
如果读者现在访问http://localhost:13380/home/edit/300,会返回500异常错误。但是访问http://localhost:13380/Home/Details/300,会返回404异常错误。这是为什么呢?
原因是访问当前的Detail视图时,判断student为null时会主动跳转到StudentNotFound视图中,但是在Edit()方法中并没有返回,而是继续使用它的属性,引发Object为null的错误,因此会触发500错误信息。
优化Edit()的代码如下。
[HttpGet]
public ViewResult Edit(int id)
{
Student student = _studentRepository.GetStudentById(id);
if(student == null)
{
Response.StatusCode = 404;
return View("StudentNotFound",id);
}
StudentEditViewModel studentEditViewModel = new StudentEditViewModel
{
Id = student.Id,
Name = student.Name,
Email = student.Email,
Major = student.Major,
ExistingPhotoPath = student.PhotoPath
};
return View(studentEditViewModel);
}
而在Startup.cs中Configure的代码如下。
public void Configure(IApplicationBuilder app,IWebHostEnvironment env)
{
//如果环境是Development serve Developer Exception Page
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//否则显示用户友好的错误页面
else if(env.IsStaging() || env.IsProduction() || env.IsEnvironment("UAT"))
{
app.UseExceptionHandler("/Error");
app.UseStatusCodePagesWithReExecute("/Error/{0}");
}
//使用纯静态文件支持的中间件,而不使用带有终端的中间件
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name:"default",
pattern:"{controller=Home}/{action=Index}/{id?}");
});
}
本章我们为系统实现了用户友好的错误页面信息以及对全局异常的处理,讲解了为实现这些功能都提供了哪些中间件,以及中间件的特点和加载顺序。在接下来的章节中,我们将会实现将异常信息记录到文件中,这样便于开发人员分析这些错误信息,以优化系统。
本文摘自《深入浅出 ASP.NET Core》
本书是一本系统地介绍ASP.NET Core、Entity Framework Core以及ASP.NET Core Identity框架技术的入门图书,旨在帮助读者循序渐进地了解和掌握ASP.NET Core。本书使用ASP.NET Core从零开始搭建一个实际的项目。从基本的控制台应用程序开始,介绍ASP.NET Core基本的启动流程,涵盖ASP.NET Core框架中各个技术的实际应用。同时,本书也会介绍一些ASP.NET Core的高级概念。在本书中,我们会开发一个学校管理系统,其中包含清晰的操作步骤和大量的实际代码,以帮助读者学以致用,将ASP.NET Core的知识运用到实际的项目开发当中,最后我们会将开发的项目部署到生产环境中。通过阅读本书,读者将掌握使用ASP.NET Core开发Web应用程序的方法,并能够在对新项目进行技术选型时做出战略决策。
本书适合有一定C# 编程经验和HTML、JavaScript基础,并对ASP.NET Core感兴趣的读者阅读,也可以作为高等院校相关专业的教学用书和培训学校的教材。
,