关于http状态代码404:如何在ASP.NET MVC中正确处理404?

How can I properly handle 404 in ASP.NET MVC?

我正在使用RC2

使用URL路由:

1
2
3
4
5
routes.MapRoute(
   "Error",
    "{*url}",
     new { controller ="Errors", action ="NotFound" }  // 404s
);

上面似乎处理了这样的请求(假设初始MVC项目设置的默认路由表):"/blah/blah/blah/blah"

重写控制器本身中的handleUnknownAction():

1
2
3
4
5
// 404s - handle here (bad action requested
protected override void HandleUnknownAction(string actionName) {
    ViewData["actionName"] = actionName;
    View("NotFound").ExecuteResult(this.ControllerContext);
}

但是,以前的策略不处理对坏的/未知控制器的请求。例如,我没有"/idNotexist",如果我请求这样做,我将从Web服务器获取一般404页,如果我使用路由+覆盖,则不会获取我的404页。

最后,我的问题是:有没有任何方法可以使用路由或MVC框架本身中的其他方法来捕获这种类型的请求?

或者我应该默认使用web.config客户错误作为我的404处理程序,然后忘记所有这些吗?我假设如果我使用customerrrors,由于web.config对直接访问的限制,我将不得不将一般404页存储在/views之外。


代码取自http://blogs.microsoft.co.il/blogs/shay/archive/2009/03/06/real-world-error-hadling-in-asp-net-mvc-rc2.aspx,也适用于asp.net mvc 1.0。

以下是我如何处理HTTP异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
protected void Application_Error(object sender, EventArgs e)
{
   Exception exception = Server.GetLastError();
   // Log the exception.

   ILogger logger = Container.Resolve<ILogger>();
   logger.Error(exception);

   Response.Clear();

   HttpException httpException = exception as HttpException;

   RouteData routeData = new RouteData();
   routeData.Values.Add("controller","Error");

   if (httpException == null)
   {
       routeData.Values.Add("action","Index");
   }
   else //It's an Http Exception, Let's handle it.
   {
       switch (httpException.GetHttpCode())
       {
          case 404:
              // Page not found.
              routeData.Values.Add("action","HttpError404");
              break;
          case 500:
              // Server error.
              routeData.Values.Add("action","HttpError500");
              break;

           // Here you can handle Views to other error codes.
           // I choose a General error template  
           default:
              routeData.Values.Add("action","General");
              break;
      }
  }          

  // Pass exception details to the target error View.
  routeData.Values.Add("error", exception);

  // Clear the error on server.
  Server.ClearError();

  // Avoid IIS7 getting in the middle
  Response.TrySkipIisCustomErrors = true;

  // Call target Controller and pass the routeData.
  IController errorController = new ErrorController();
  errorController.Execute(new RequestContext(    
       new HttpContextWrapper(Context), routeData));
}


404的要求

下面是我对404解决方案的需求,下面我将展示如何实现它:

  • 我想处理与不良行为匹配的路线
  • 我想用坏的控制器处理匹配的路由
  • 我想处理不匹配的路由(我的应用程序无法理解的任意URL)-我不希望这些冒泡到global.asax或iis,因为这样我就无法正确地重定向回我的MVC应用程序。
  • 我想用和上面一样的方式处理自定义404——比如当为一个不存在的对象(可能被删除)提交一个ID时。
  • 我希望我的所有404返回MVC视图(而不是静态页面),如果需要,我可以稍后将更多数据泵送到该视图(良好的404设计),并且它们必须返回HTTP 404状态代码。

解决方案

我认为您应该在global.asax中保存Application_Error,以便进行更高级别的操作,例如未处理的异常和日志记录(如Shay Jacoby的回答显示),但不应保存404处理。这就是为什么我的建议将404文件从global.asax文件中删除的原因。

第一步:有404错误逻辑的共同点

这是维护性的一个好主意。使用一个错误控制器,以便将来对设计良好的404页的改进可以很容易地适应。另外,确保您的响应具有404代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ErrorController : MyController
{
    #region Http404

    public ActionResult Http404(string url)
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        var model = new NotFoundViewModel();
        // If the url is relative ('NotFound' route) then replace with Requested path
        model.RequestedUrl = Request.Url.OriginalString.Contains(url) & Request.Url.OriginalString != url ?
            Request.Url.OriginalString : url;
        // Dont get the user stuck in a 'retry loop' by
        // allowing the Referrer to be the same as the Request
        model.ReferrerUrl = Request.UrlReferrer != null &&
            Request.UrlReferrer.OriginalString != model.RequestedUrl ?
            Request.UrlReferrer.OriginalString : null;

        // TODO: insert ILogger here

        return View("NotFound", model);
    }
    public class NotFoundViewModel
    {
        public string RequestedUrl { get; set; }
        public string ReferrerUrl { get; set; }
    }

    #endregion
}

步骤2:使用一个基本控制器类,这样您就可以轻松地调用自定义404操作并连接HandleUnknownAction

ASP.NET MVC中的404需要在多个位置捕获。第一个是HandleUnknownAction

InvokeHttp404方法为重新路由到ErrorController和我们的新Http404操作创建了一个公共位置。干杯!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class MyController : Controller
{
    #region Http404 handling

    protected override void HandleUnknownAction(string actionName)
    {
        // If controller is ErrorController dont 'nest' exceptions
        if (this.GetType() != typeof(ErrorController))
            this.InvokeHttp404(HttpContext);
    }

    public ActionResult InvokeHttp404(HttpContextBase httpContext)
    {
        IController errorController = ObjectFactory.GetInstance<ErrorController>();
        var errorRoute = new RouteData();
        errorRoute.Values.Add("controller","Error");
        errorRoute.Values.Add("action","Http404");
        errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
        errorController.Execute(new RequestContext(
             httpContext, errorRoute));

        return new EmptyResult();
    }

    #endregion
}

步骤3:在控制器工厂中使用依赖注入,并连接404个httpexceptions

就像这样(它不必是结构图):

MVC1.0示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class StructureMapControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(controllerType);
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
            {
                IController errorController = ObjectFactory.GetInstance<ErrorController>();
                ((ErrorController)errorController).InvokeHttp404(RequestContext.HttpContext);

                return errorController;
            }
            else
                throw ex;
        }

        return ObjectFactory.GetInstance(controllerType) as Controller;
    }
}

MVC2.0示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(requestContext, controllerType);
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == 404)
            {
                IController errorController = ObjectFactory.GetInstance<ErrorController>();
                ((ErrorController)errorController).InvokeHttp404(requestContext.HttpContext);

                return errorController;
            }
            else
                throw ex;
        }

        return ObjectFactory.GetInstance(controllerType) as Controller;
    }

我认为最好是在接近错误来源的地方发现错误。这就是为什么我更喜欢上面的Application_Error处理程序。

这是第二个抓到404的地方。

步骤4:为未能解析到应用程序中的URL向global.asax添加未找到的路由

这条路线应该指向我们的Http404行动。注意,url参数将是一个相对的URL,因为路由引擎正在剥离这里的域部分?这就是为什么我们在步骤1中拥有所有条件URL逻辑的原因。

1
2
        routes.MapRoute("NotFound","{*url}",
            new { controller ="Error", action ="Http404" });

这是第三个也是最后一个在MVC应用程序中捕获404的地方,而您不调用自己。如果您不在这里捕获不匹配的路由,那么MVC将把问题传递给ASP.NET(global.asax),在这种情况下,您并不真正希望这样做。

第五步:最后,当你的应用找不到东西时调用404。

就像当一个坏身份被提交给我的贷款控制者(来自MyController)一样:

1
2
3
4
5
6
7
8
9
10
11
    //
    // GET: /Detail/ID

    public ActionResult Detail(int ID)
    {
        Loan loan = this._svc.GetLoans().WithID(ID);
        if (loan == null)
            return this.InvokeHttp404(HttpContext);
        else
            return View(loan);
    }

如果所有这些都能用更少的代码连接到更少的地方,那就太好了,但是我认为这个解决方案更易于维护,更易于测试,而且相当实用。

感谢您迄今为止的反馈。我很想得到更多。

注意:这是从我的原始答案中显著编辑的,但目的/要求是相同的-这就是为什么我没有添加新的答案的原因。


ASP.NET MVC不太支持自定义404页。自定义控制器工厂,捕获所有路由,带HandleUnknownAction的基本控制器类-argh!

到目前为止,IIS自定义错误页是更好的选择:

Web.CONFIG

1
2
3
4
5
6
<system.webServer>
  <httpErrors errorMode="Custom" existingResponse="Replace">
    <remove statusCode="404" />
    <error statusCode="404" responseMode="ExecuteURL" path="/Error/PageNotFound" />
  </httpErrors>
</system.webServer>

错误控制器

1
2
3
4
5
6
7
8
public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View();
    }
}

样例工程

  • Github上的test404
  • 现场网站


快速回答/tl;dr

enter image description here

对于那些懒惰的人:

1
Install-Package MagicalUnicornMvcErrorToolkit -Version 1.0

然后从global.asax上取下这条线。

1
GlobalFilters.Filters.Add(new HandleErrorAttribute());

这只适用于iis7+和iis Express。

如果你用卡西尼……好。。嗯。。呃。。笨拙的…awkward

解释了很久的答案

我知道这已经被回答了。但答案很简单(为大卫·福勒和达米安·爱德华兹的回答干杯)。

没有必要做任何定制。

对于ASP.NET MVC3来说,所有的碎片都在那里。

步骤1->在两个地方更新web.config。

1
2
3
4
<system.web>
    <customErrors mode="On" defaultRedirect="/ServerError">
      <error statusCode="404" redirect="/NotFound" />
    </customErrors>

1
2
3
4
5
6
7
8
9
10
11
12
<system.webServer>
    <httpErrors errorMode="Custom">
      <remove statusCode="404" subStatusCode="-1" />
      <error statusCode="404" path="/NotFound" responseMode="ExecuteURL" />
      <remove statusCode="500" subStatusCode="-1" />
      <error statusCode="500" path="/ServerError" responseMode="ExecuteURL" />
    </httpErrors>    

...
<system.webServer>
...
</system.web>

现在请注意我决定使用的路线。你可以用任何东西,但我的路线是

  • 对于404未找到,错误页。
  • 对于任何其他错误,包括在我的代码中发生的错误。这是一个500内部服务器错误

看看中的第一部分如何只有一个海关条目?statusCode="404"条目?我只列出了一个状态代码,因为所有其他错误,包括500 Server Error(即当您的代码出现错误并破坏用户的请求时发生的令人讨厌的错误)。所有其他错误由设置defaultRedirect="/ServerError"处理。上面写着,如果没有找到404页,请转到/ServerError号路线。

好啊。别挡道……现在到我在global.asax中列出的路线

步骤2-在global.asax中创建路由

这是我的全程路段……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.IgnoreRoute("{*favicon}", new {favicon = @"(.*/)?favicon.ico(/.*)?"});

    routes.MapRoute(
       "Error - 404",
       "NotFound",
        new { controller ="Error", action ="NotFound" }
        );

    routes.MapRoute(
       "Error - 500",
       "ServerError",
        new { controller ="Error", action ="ServerError"}
        );

    routes.MapRoute(
       "Default", // Route name
       "{controller}/{action}/{id}", // URL with parameters
        new {controller ="Home", action ="Index", id = UrlParameter.Optional}
        );
}

这列出了两条忽略路径->axd'sfavicons(oo!奖励忽略路线,为您!)然后(这里命令是必须的),我有两个明确的错误处理路径。然后是其他路线。在这种情况下,是默认的。当然,我有更多,但这对我的网站来说是特别的。只要确保错误路径在列表的顶部就行了。命令是必须的。

最后,当我们在global.asax文件中时,我们不会全局注册handleError属性。不,不,不,先生。纳达。不.Nien。否定的。真的…

从EDOCX1[1]中删除此行

1
GlobalFilters.Filters.Add(new HandleErrorAttribute());

步骤3-使用操作方法创建控制器

现在。。我们添加了一个有两种操作方法的控制器…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ErrorController : Controller
{
    public ActionResult NotFound()
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        return View();
    }

    public ActionResult ServerError()
    {
        Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        // Todo: Pass the exception into the view model, which you can make.
        //       That's an exercise, dear reader, for -you-.
        //       In case u want to pass it to the view, if you're admin, etc.
        // if (User.IsAdmin) // <-- I just made that up :) U get the idea...
        // {
        //     var exception = Server.GetLastError();
        //     // etc..
        // }

        return View();
    }

    // Shhh .. secret test method .. ooOOooOooOOOooohhhhhhhh
    public ActionResult ThrowError()
    {
        throw new NotImplementedException("Pew ^ Pew");
    }
}

好的,让我们看看这个。首先,这里没有[HandleError]属性。为什么?因为内置的ASP.NET框架已经在处理错误,而且我们已经指定了处理错误所需的所有操作:)它就在这个方法中!

接下来,我有两种操作方法。没有什么困难。如果您希望显示任何异常信息,那么您可以使用Server.GetLastError()获取该信息。

额外的WTF:是的,我做了第三个操作方法来测试错误处理。

步骤4-创建视图

最后,创建两个视图。把它们放在这个控制器的正常视点。

enter image description here

奖金点评

  • 你不需要Application_Error(object sender, EventArgs e)
  • 以上步骤与Elmah 100%完美配合。埃尔玛·弗劳克斯!

我的朋友们,应该是这样。

现在,恭喜你读了这么多,并有一只独角兽作为奖品!

enter image description here


我研究了很多关于如何在MVC(特别是MVC3)中正确管理404的问题,并且,IMHO是我提出的最佳解决方案:

在Axal.Axax中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MvcApplication : HttpApplication
{
    protected void Application_EndRequest()
    {
        if (Context.Response.StatusCode == 404)
        {
            Response.Clear();

            var rd = new RouteData();
            rd.DataTokens["area"] ="AreaName"; // In case controller is in another area
            rd.Values["controller"] ="Errors";
            rd.Values["action"] ="NotFound";

            IController c = new ErrorsController();
            c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
        }
    }
}

错误控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public sealed class ErrorsController : Controller
{
    public ActionResult NotFound()
    {
        ActionResult result;

        object model = Request.Url.PathAndQuery;

        if (!Request.IsAjaxRequest())
            result = View(model);
        else
            result = PartialView("_NotFound", model);

        return result;
    }
}

(可选)

说明:

在AFAIK中,有6种不同的情况下,ASP.NET MVC3应用程序可以生成404。

(由ASP.NET框架自动生成:)

(1)URL在路由表中找不到匹配项。

(由ASP.NET MVC框架自动生成:)

(2)URL在路由表中找到匹配项,但指定了不存在的控制器。

(3)URL在路由表中找到匹配项,但指定了一个不存在的操作。

(手动生成:)

(4)操作使用httpNotFound()方法返回httpNotFoundResult。

(5)操作引发一个状态代码为404的httpexception。

(6)操作手动将response.statuscode属性修改为404。

通常,您希望实现3个目标:

(1)向用户显示自定义404错误页。

(2)在客户端响应上维护404状态码(对SEO特别重要)。

(3)直接发送响应,不涉及302重定向。

实现这一目标的方法多种多样:

(1)

1
2
3
4
5
<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customError>
</system.web>

此解决方案的问题:

  • 在第(1)、(4)、(6)种情况下不符合目标(1)。
  • 不自动符合目标(2)。必须手动编程。
  • 不符合目标(3)。
  • (2)

    1
    2
    3
    4
    5
    6
    <system.webServer>
        <httpErrors errorMode="Custom">
            <remove statusCode="404"/>
            <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
        </httpErrors>
    </system.webServer>

    此解决方案的问题:

  • 仅适用于IIS 7+。
  • 在第(2)、(3)、(5)种情况下不符合目标(1)。
  • 不自动符合目标(2)。必须手动编程。
  • (3)

    1
    2
    3
    4
    5
    6
    <system.webServer>
        <httpErrors errorMode="Custom" existingResponse="Replace">
            <remove statusCode="404"/>
            <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
        </httpErrors>
    </system.webServer>

    此解决方案的问题:

  • 仅适用于IIS 7+。
  • 不自动符合目标(2)。必须手动编程。
  • 它隐藏应用程序级HTTP异常。例如,不能使用customErrors节、system.web.mvc.handleErrorAttribute等。它不能只显示一般错误页。
  • (4)

    1
    2
    3
    4
    5
    <system.web>
        <customErrors mode="On">
            <error statusCode="404" redirect="~/Errors/NotFound"/>
        </customError>
    </system.web>

    1
    2
    3
    4
    5
    6
    <system.webServer>
        <httpErrors errorMode="Custom">
            <remove statusCode="404"/>
            <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
        </httpErrors>
    </system.webServer>

    此解决方案的问题:

  • 仅适用于IIS 7+。
  • 不自动符合目标(2)。必须手动编程。
  • 在第(2)、(3)、(5)种情况下不符合目标(3)。
  • 以前遇到过这种问题的人甚至试图创建自己的库(请参阅http://aboutcode.net/2011/02/26/handling-not-found-with-asp-net-mvc3.html)。但是,前面的解决方案似乎涵盖了所有的案例,而没有使用外部库的复杂性。


    我真的很喜欢Cotsaks解决方案,并且认为它的解释非常清楚。我唯一的补充是改变步骤2如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public abstract class MyController : Controller
    {

        #region Http404 handling

        protected override void HandleUnknownAction(string actionName)
        {
            //if controller is ErrorController dont 'nest' exceptions
            if(this.GetType() != typeof(ErrorController))
            this.InvokeHttp404(HttpContext);
        }

        public ActionResult InvokeHttp404(HttpContextBase httpContext)
        {
            IController errorController = ObjectFactory.GetInstance<ErrorController>();
            var errorRoute = new RouteData();
            errorRoute.Values.Add("controller","Error");
            errorRoute.Values.Add("action","Http404");
            errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
            errorController.Execute(new RequestContext(
                 httpContext, errorRoute));

            return new EmptyResult();
        }

        #endregion
    }

    基本上,这会阻止包含无效操作和控制器的URL两次触发异常例程。例如,对于URL,如asdfdf/dfgdfgd


    我能让@cottsak的方法用于无效控制器的唯一方法是修改CustomControllerFactory中的现有路由请求,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class CustomControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            try
            {
                if (controllerType == null)
                    return base.GetControllerInstance(requestContext, controllerType);
                else
                    return ObjectFactory.GetInstance(controllerType) as Controller;
            }
            catch (HttpException ex)
            {
                if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
                {
                    requestContext.RouteData.Values["controller"] ="Error";
                    requestContext.RouteData.Values["action"] ="Http404";
                    requestContext.RouteData.Values.Add("url", requestContext.HttpContext.Request.Url.OriginalString);

                    return ObjectFactory.GetInstance<ErrorController>();
                }
                else
                    throw ex;
            }
        }
    }

    我应该提到我正在使用MVC 2.0。


    我的简短解决方案可以处理未处理的区域、控制器和操作:

  • 创建一个视图404.cshtml。

  • 为控制器创建一个基类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Controller : System.Web.Mvc.Controller
    {
        protected override void HandleUnknownAction(string actionName)
        {
            Http404().ExecuteResult(ControllerContext);
        }

        protected virtual ViewResult Http404()
        {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return View("404");
        }
    }
  • 创建一个自定义控制器工厂,将基本控制器返回为回退:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class ControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType != null)
                return base.GetControllerInstance(requestContext, controllerType);

            return new Controller();
        }
    }
  • Application_Start()中增加以下行:

    1
    ControllerBuilder.Current.SetControllerFactory(typeof(ControllerFactory));

  • 这里是另一个使用MVC工具的方法,您可以处理对坏控制器名、坏路由名以及您认为适合于操作方法内部的任何其他条件的请求。就我个人而言,我更喜欢避免尽可能多的web.config设置,因为它们执行302/200重定向,不支持使用Razor视图的ResponseRewrite(Server.Transfer)。出于SEO的原因,我宁愿返回一个带有自定义错误页面的404。

    其中一些是对Cotsak上述技术的新看法。

    此解决方案还使用最少量的web.config设置来支持MVC 3错误过滤器。

    用法

    只需从操作或自定义actionfilterattribute中引发httpException。

    1
    Throw New HttpException(HttpStatusCode.NotFound,"[Custom Exception Message Here]")

    步骤1

    将以下设置添加到web.config。这是使用MVC的handleErrorAttribute所必需的。

    1
    <customErrors mode="On" redirectMode="ResponseRedirect" />

    步骤2

    添加一个类似于MVC框架的handleErrorAttribute的自定义handleHttperRorrAttribute,HTTP错误除外:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    <AttributeUsage(AttributeTargets.All, AllowMultiple:=True)>
    Public Class HandleHttpErrorAttribute
        Inherits FilterAttribute
        Implements IExceptionFilter

        Private Const m_DefaultViewFormat As String ="ErrorHttp{0}"

        Private m_HttpCode As HttpStatusCode
        Private m_Master As String
        Private m_View As String

        Public Property HttpCode As HttpStatusCode
            Get
                If m_HttpCode = 0 Then
                    Return HttpStatusCode.NotFound
                End If
                Return m_HttpCode
            End Get
            Set(value As HttpStatusCode)
                m_HttpCode = value
            End Set
        End Property

        Public Property Master As String
            Get
                Return If(m_Master, String.Empty)
            End Get
            Set(value As String)
                m_Master = value
            End Set
        End Property

        Public Property View As String
            Get
                If String.IsNullOrEmpty(m_View) Then
                    Return String.Format(m_DefaultViewFormat, Me.HttpCode)
                End If
                Return m_View
            End Get
            Set(value As String)
                m_View = value
            End Set
        End Property

        Public Sub OnException(filterContext As System.Web.Mvc.ExceptionContext) Implements System.Web.Mvc.IExceptionFilter.OnException
            If filterContext Is Nothing Then Throw New ArgumentException("filterContext")

            If filterContext.IsChildAction Then
                Return
            End If

            If filterContext.ExceptionHandled OrElse Not filterContext.HttpContext.IsCustomErrorEnabled Then
                Return
            End If

            Dim ex As HttpException = TryCast(filterContext.Exception, HttpException)
            If ex Is Nothing OrElse ex.GetHttpCode = HttpStatusCode.InternalServerError Then
                Return
            End If

            If ex.GetHttpCode <> Me.HttpCode Then
                Return
            End If

            Dim controllerName As String = filterContext.RouteData.Values("controller")
            Dim actionName As String = filterContext.RouteData.Values("action")
            Dim model As New HandleErrorInfo(filterContext.Exception, controllerName, actionName)

            filterContext.Result = New ViewResult With {
                .ViewName = Me.View,
                .MasterName = Me.Master,
                .ViewData = New ViewDataDictionary(Of HandleErrorInfo)(model),
                .TempData = filterContext.Controller.TempData
            }
            filterContext.ExceptionHandled = True
            filterContext.HttpContext.Response.Clear()
            filterContext.HttpContext.Response.StatusCode = Me.HttpCode
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = True
        End Sub
    End Class

    步骤3

    Global.asax中的globalfiltercollection(GlobalFilters.Filters添加过滤器。此示例将把所有InternalServerError(500)错误路由到错误共享视图(Views/Shared/Error.vbhtml)。NotFound(404)错误也将发送到共享视图中的errorhttp404.vbhtml。我在这里添加了一个401错误,向您展示如何将其扩展为其他HTTP错误代码。请注意,这些必须是共享视图,它们都使用System.Web.Mvc.HandleErrorInfo对象作为模型。

    1
    2
    3
    filters.Add(New HandleHttpErrorAttribute With {.View ="ErrorHttp401", .HttpCode = HttpStatusCode.Unauthorized})
    filters.Add(New HandleHttpErrorAttribute With {.View ="ErrorHttp404", .HttpCode = HttpStatusCode.NotFound})
    filters.Add(New HandleErrorAttribute With {.View ="Error"})

    步骤4

    创建一个基本控制器类,并在控制器中从该类继承。此步骤允许我们处理未知的操作名,并将HTTP 404错误提升到handlehtperrorrattribute。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Public Class BaseController
        Inherits System.Web.Mvc.Controller

        Protected Overrides Sub HandleUnknownAction(actionName As String)
            Me.ActionInvoker.InvokeAction(Me.ControllerContext,"Unknown")
        End Sub

        Public Function Unknown() As ActionResult
            Throw New HttpException(HttpStatusCode.NotFound,"The specified controller or action does not exist.")
            Return New EmptyResult
        End Function
    End Class

    步骤5

    创建一个ControllerFactory覆盖,并在应用程序_start的global.asax文件中覆盖它。此步骤允许我们在指定无效的控制器名称时引发HTTP 404异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Public Class MyControllerFactory
        Inherits DefaultControllerFactory

        Protected Overrides Function GetControllerInstance(requestContext As System.Web.Routing.RequestContext, controllerType As System.Type) As System.Web.Mvc.IController
            Try
                Return MyBase.GetControllerInstance(requestContext, controllerType)
            Catch ex As HttpException
                Return DependencyResolver.Current.GetService(Of BaseController)()
            End Try
        End Function
    End Class

    'In Global.asax.vb Application_Start:

    controllerBuilder.Current.SetControllerFactory(New MyControllerFactory)

    步骤6

    在路由表中包含一个特殊路由。用于basecontroller未知操作的路由。这将帮助我们在用户访问未知控制器或未知操作的情况下引发404。

    1
    2
    3
    4
    5
    'BaseController
    routes.MapRoute( _
       "Unknown","BaseController/{action}/{id}", _
        New With {.controller ="BaseController", .action ="Unknown", .id = UrlParameter.Optional} _
    )

    总结

    这个例子演示了如何使用MVC框架将404个HTTP错误代码返回到浏览器,而不使用过滤器属性和共享错误视图进行重定向。它还演示了在指定无效的控制器名和操作名时显示相同的自定义错误页。

    我将添加一个无效控制器名称、操作名称和从home/triggerNotFound操作引发的自定义404的屏幕截图,如果我获得足够的投票来发布一个=)。使用此解决方案访问以下URL时,Fiddler返回404消息:

    1
    2
    3
    4
    /InvalidController
    /Home/InvalidRoute
    /InvalidController/InvalidRoute
    /Home/TriggerNotFound

    Cotsak在上面的文章和这些文章都是很好的参考。

    • 使用customErrors redirectMode=responseRewrite时出现问题
    • Elmah+MVC手柄箭头属性


    在MVC4中,WebAPI 404的处理方式如下:

    课程顶点控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        // GET /api/courses/5
        public HttpResponseMessage<Courses> Get(int id)
        {
            HttpResponseMessage<Courses> resp = null;

            var aCourse = _courses.Where(c => c.Id == id).FirstOrDefault();

            resp = aCourse == null ? new HttpResponseMessage<Courses>(System.Net.HttpStatusCode.NotFound) : new HttpResponseMessage<Courses>(aCourse);

            return resp;
        }

    家庭控制器

    1
    2
    3
    4
    public ActionResult Course(int id)
    {
        return View(id);
    }

    视图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <script type="text/javascript">
        var id = @Model;
        var course = $('#course');
        $.ajax({    
            url: '/api/courses/' + id,
            success: function (data) {
                course.text(data.Name);
            },
            statusCode: {
                404: function()
                {
                    course.text('Course not available!');    
                }
            }
        });

    全球的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapHttpRoute(
            name:"DefaultApi",
            routeTemplate:"api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        routes.MapRoute(
            name:"Default",
            url:"{controller}/{action}/{id}",
            defaults: new { controller ="Home", action ="Index", id = UrlParameter.Optional }
        );
    }

    结果

    enter image description here


    我的解决方案,以防有人发现它有用。

    Web.config:

    1
    2
    3
    4
    5
    6
    <system.web>
        <customErrors mode="On" defaultRedirect="Error">
          <error statusCode="404" redirect="~/Error/PageNotFound"/>
        </customErrors>
        ...
    </system.web>

    Controllers/ErrorController.cs中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ErrorController : Controller
    {
        public ActionResult PageNotFound()
        {
            if(Request.IsAjaxRequest()) {
                Response.StatusCode = (int)HttpStatusCode.NotFound;
                return Content("Not Found","text/plain");
            }

            return View();
        }
    }

    Shared文件夹中添加一个PageNotFound.cshtml,就这样了。


    不过,在我看来,标准的customErrors配置应该可以正常工作,因为依赖Server.Transfer,所以ResponseRewrite的内部实现似乎与MVC不兼容。

    我觉得这是一个功能漏洞,所以我决定使用HTTP模块重新实现这个功能。下面的解决方案允许您通过重定向到任何有效的MVC路由来处理任何HTTP状态代码(包括404),就像您通常做的那样。

    1
    2
    3
    4
    <customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
        <error statusCode="404" redirect="404.aspx" />
        <error statusCode="500" redirect="~/MVCErrorPage" />
    </customErrors>

    已经在以下平台上进行了测试;

    • MVC4处于集成管道模式(IIS Express 8)
    • 经典模式下的MVC4(与开发服务器、Cassini相比)
    • 经典模式下的MVC4(IIS6)

    效益

    • 可以放到任何MVC项目中的通用解决方案
    • 支持传统的自定义错误配置
    • 在集成管道和经典模式下工作

    解决方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    namespace Foo.Bar.Modules {

        /// <summary>
        /// Enables support for CustomErrors ResponseRewrite mode in MVC.
        /// </summary>
        public class ErrorHandler : IHttpModule {

            private HttpContext HttpContext { get { return HttpContext.Current; } }
            private CustomErrorsSection CustomErrors { get; set; }

            public void Init(HttpApplication application) {
                System.Configuration.Configuration configuration = WebConfigurationManager.OpenWebConfiguration("~");
                CustomErrors = (CustomErrorsSection)configuration.GetSection("system.web/customErrors");

                application.EndRequest += Application_EndRequest;
            }

            protected void Application_EndRequest(object sender, EventArgs e) {

                // only handle rewrite mode, ignore redirect configuration (if it ain't broke don't re-implement it)
                if (CustomErrors.RedirectMode == CustomErrorsRedirectMode.ResponseRewrite && HttpContext.IsCustomErrorEnabled) {

                    int statusCode = HttpContext.Response.StatusCode;

                    // if this request has thrown an exception then find the real status code
                    Exception exception = HttpContext.Error;
                    if (exception != null) {
                        // set default error status code for application exceptions
                        statusCode = (int)HttpStatusCode.InternalServerError;
                    }

                    HttpException httpException = exception as HttpException;
                    if (httpException != null) {
                        statusCode = httpException.GetHttpCode();
                    }

                    if ((HttpStatusCode)statusCode != HttpStatusCode.OK) {

                        Dictionary<int, string> errorPaths = new Dictionary<int, string>();

                        foreach (CustomError error in CustomErrors.Errors) {
                            errorPaths.Add(error.StatusCode, error.Redirect);
                        }

                        // find a custom error path for this status code
                        if (errorPaths.Keys.Contains(statusCode)) {
                            string url = errorPaths[statusCode];

                            // avoid circular redirects
                            if (!HttpContext.Request.Url.AbsolutePath.Equals(VirtualPathUtility.ToAbsolute(url))) {

                                HttpContext.Response.Clear();
                                HttpContext.Response.TrySkipIisCustomErrors = true;

                                HttpContext.Server.ClearError();

                                // do the redirect here
                                if (HttpRuntime.UsingIntegratedPipeline) {
                                    HttpContext.Server.TransferRequest(url, true);
                                }
                                else {
                                    HttpContext.RewritePath(url, false);

                                    IHttpHandler httpHandler = new MvcHttpHandler();
                                    httpHandler.ProcessRequest(HttpContext);
                                }

                                // return the original status code to the client
                                // (this won't work in integrated pipleline mode)
                                HttpContext.Response.StatusCode = statusCode;

                            }
                        }

                    }

                }

            }

            public void Dispose() {

            }


        }

    }

    用法

    将其作为web.config中的最终HTTP模块包含在内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      <system.web>
        <httpModules>
         
        </httpModules>
      </system.web>

      <!-- IIS7+ -->
      <system.webServer>
        <modules>
         
        </modules>
      </system.webServer>

    对于那些关注的人,您会注意到,在集成管道模式下,由于Server.TransferRequest的工作方式,这将始终使用HTTP 200进行响应。要返回正确的错误代码,我使用以下错误控制器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class ErrorController : Controller {

        public ErrorController() { }

        public ActionResult Index(int id) {
            // pass real error code to client
            HttpContext.Response.StatusCode = id;
            HttpContext.Response.TrySkipIisCustomErrors = true;

            return View("Errors/" + id.ToString());
        }

    }

    在ASP.NET MVC中处理错误只是一个麻烦。我在这个页面和其他问题和网站上尝试了很多建议,但没有什么好的。一个建议是在system.webserver内部处理web.config上的错误,但只返回空白页。

    我提出这个解决方案的目标是:

    • 不重定向
    • 返回正确的状态代码不是200/OK,如默认错误处理

    这是我的解决方案。

    1.将以下内容添加到system.web节

    1
    2
    3
    4
    5
    6
       <system.web>
         <customErrors mode="On" redirectMode="ResponseRewrite">
          <error statusCode="404"  redirect="~/Error/404.aspx" />
          <error statusCode="500" redirect="~/Error/500.aspx" />
         </customErrors>
        <system.web>

    上面处理了routes.config未处理的任何URL和未处理的异常,特别是在视图上遇到的异常。注意,我使用的是ASPX而不是HTML。这样我就可以在后面的代码上添加响应代码。

    2。在项目根目录下创建一个名为"错误"的文件夹(或您喜欢的任何文件夹),并添加这两个Web窗体。下面是我的404页;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="404.aspx.cs" Inherits="Myapp.Error._404" %>

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title >Page Not found
        <link href="<%=ResolveUrl("~/Content/myapp.css")%>" rel="stylesheet" />
    </head>
    <body>
       
         
       
       
            404 - Page Not found
            <p>
    The page you are looking for cannot be found.
    </p>
            <hr />
            <footer></footer>
       
    </body>
    </html>

    在后面的代码上,我设置了响应代码

    1
    2
    3
    4
    protected void Page_Load(object sender, EventArgs e)
    {
        Response.StatusCode = 404;
    }

    500页也一样

    3.处理控制器内的错误。有很多方法可以做到。这就是我的工作。我的所有控制器都继承自一个基本控制器。在基本控制器中,我有以下方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    protected ActionResult ShowNotFound()
    {
        return ShowNotFound("Page not found....");
    }

    protected ActionResult ShowNotFound(string message)
    {
        return ShowCustomError(HttpStatusCode.NotFound, message);
    }

    protected ActionResult ShowServerError()
    {
        return ShowServerError("Application error....");
    }

    protected ActionResult ShowServerError(string message)
    {
        return ShowCustomError(HttpStatusCode.InternalServerError, message);
    }

    protected ActionResult ShowNotAuthorized()
    {
        return ShowNotAuthorized("You are not allowed ....");

    }

    protected ActionResult ShowNotAuthorized(string message)
    {
        return ShowCustomError(HttpStatusCode.Forbidden, message);
    }

    protected ActionResult ShowCustomError(HttpStatusCode statusCode, string message)
    {
        Response.StatusCode = (int)statusCode;
        string title ="";
        switch (statusCode)
        {
            case HttpStatusCode.NotFound:
                title ="404 - Not found";
                break;
            case HttpStatusCode.Forbidden:
                title ="403 - Access Denied";
                break;
            default:
                title ="500 - Application Error";
                break;
        }
        ViewBag.Title = title;
        ViewBag.Message = message;
        return View("CustomError");
    }

    4.将customerrror.cshtml添加到共享视图文件夹中。下面是我的;

    1
    2
    3
    4
    5
    @ViewBag.Title
    <br />
    <p>
    @ViewBag.Message
    </p>

    现在,在应用程序控制器中,您可以执行类似的操作;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class WidgetsController : ControllerBase
    {
      [HttpGet]
      public ActionResult Edit(int id)
      {
        Try
        {
           var widget = db.getWidgetById(id);
           if(widget == null)
              return ShowNotFound();
              //or return ShowNotFound("Invalid widget!");
           return View(widget);
        }
        catch(Exception ex)
        {
           //log error
           logger.Error(ex)
           return ShowServerError();
        }
      }
    }

    现在,请注意。它不会处理静态文件错误。因此,如果您有一个路由,例如example.com/widgets,并且用户将其更改为example.com/widgets.html,那么他们将获得IIS默认错误页,因此您必须以其他方式处理IIS级别的错误。


    在Nuget上尝试NotFoundMVC。它工作,没有设置。


    1)生成抽象控制器类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public abstract class MyController:Controller
    {
        public ActionResult NotFound()
        {
            Response.StatusCode = 404;
            return View("NotFound");
        }

        protected override void HandleUnknownAction(string actionName)
        {
            this.ActionInvoker.InvokeAction(this.ControllerContext,"NotFound");
        }
        protected override void OnAuthorization(AuthorizationContext filterContext) { }
    }

    2)继承所有控制器中的这个抽象类

    1
    2
    public class HomeController : MyController
    {}

    3)并在查看共享文件夹中添加名为"未找到"的视图。


    因为我的评论太长,所以发表了一个答案…

    这是对独角兽帖子/答案的评论和问题:

    https://stackoverflow.com/a/7499406/687549

    我比其他人更喜欢这个答案,因为它很简单,而且事实上微软的一些人显然是被咨询过的。不过,我有三个问题,如果可以回答,那么我将把这个答案称为InterWebs上ASP.NET MVC(X)应用程序所有404/500错误答案的圣杯。

    @普鲁克克罗姆

  • 你能从GWB的评论中更新你的答案吗(你的答案中从来没有提到过这个问题)——

  • 你能问一下你的ASP.NET团队的朋友们,这样做是否合适吗?最好能得到一些确认,也许用这种方式改变redirectModeexistingResponse是一个很大的禁忌,以便能够很好地使用seo吗?!

  • 你能在和你在微软的朋友交谈后,对所有这些东西(customErrors redirectMode="ResponseRewrite"customErrors redirectMode="ResponseRedirect"httpErrors errorMode="Custom" existingResponse="Replace"、完全删除customErrors)补充一些说明吗?

  • 正如我所说,如果我们能让你的回答更完整,这将是一个非常受欢迎的问题,有54000多个观点。

    更新:独角兽回答是否找到302和200 OK,并且不能更改为仅使用路由返回404。它必须是一个物理文件,而不是非常mvc:ish。所以,继续寻找另一个解决方案。太糟糕了,因为这似乎是最终极的MVC:ish答案。


    添加我的解决方案,这几乎是相同的赫尔曼菅直人的,有一个小皱纹,让它工作为我的项目。

    创建自定义错误控制器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Error404Controller : BaseController
    {
        [HttpGet]
        public ActionResult PageNotFound()
        {
            Response.StatusCode = 404;
            return View("404");
        }
    }

    然后创建自定义控制器工厂:

    1
    2
    3
    4
    5
    6
    7
    public class CustomControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            return controllerType == null ? new Error404Controller() : base.GetControllerInstance(requestContext, controllerType);
        }
    }

    最后,向自定义错误控制器添加覆盖:

    1
    2
    3
    4
    5
    6
    7
    protected override void HandleUnknownAction(string actionName)
    {
        var errorRoute = new RouteData();
        errorRoute.Values.Add("controller","Error404");
        errorRoute.Values.Add("action","PageNotFound");
        new Error404Controller().Execute(new RequestContext(HttpContext, errorRoute));
    }

    就是这样。不需要更改web.config。


    我浏览了贴在这条线上的大多数解决方案。虽然这个问题可能很古老,但即使现在它仍然非常适用于新项目,所以我花了很多时间来阅读这里以及其他地方的答案。

    正如@marco指出的,404可能发生的不同情况,我检查了根据该列表一起编译的解决方案。除了他的需求清单,我还增加了一个。

    • 该解决方案应该能够以最适当的方式处理MVC以及Ajax/WebAPI调用。(例如,如果在MVC中出现404,则应显示未找到的页面;如果在WebAPI中出现404,则不应劫持XML/JSON响应,以便使用的javascript可以轻松解析它)。

    此解决方案是2倍:

    第一部分来自@guillaume,网址为https://stackoverflow.com/a/27354140/2310818。他们的解决方案负责处理由于无效路由、无效控制器和无效操作而导致的404。

    这个想法是创建一个WebForm,然后让它调用MVC错误控制器的NotFound操作。所有这些都是在没有任何重定向的情况下完成的,因此您将不会在fiddler中看到单个302。原始的URL也被保留,这使得这个解决方案非常棒!

    第二部分来自@germ_n,网址为https://stackoverflow.com/a/5536676/2310818。他们的解决方案将处理您的操作以httpNotFoundResult()或throw new httpException()形式返回的任何404!

    其思想是让一个过滤器检查响应以及MVC控制器抛出的异常,并在错误控制器中调用适当的操作。同样,此解决方案在不进行任何重定向的情况下工作,并且保留原始URL!

    正如您所看到的,这两个解决方案一起提供了一个非常健壮的错误处理机制,它们实现了@marco列出的所有需求以及我的需求。如果您希望看到这个解决方案的工作示例或演示,请在评论中留下,我将很高兴把它放在一起。


    我读过所有的文章,但没有对我有用的:我的要求用户在你的url自定义404页面中输入任何内容,我认为这是非常直接的。但是你应该正确理解404的处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     <system.web>
        <customErrors mode="On" redirectMode="ResponseRewrite">
          <error statusCode="404" redirect="~/PageNotFound.aspx"/>
        </customErrors>
      </system.web>
    <system.webServer>
        <httpErrors errorMode="Custom">
          <remove statusCode="404"/>
          <error statusCode="404" path="/PageNotFound.html" responseMode="ExecuteURL"/>
        </httpErrors>
    </system.webServer>

    我发现这篇文章很有帮助。应该立即阅读。客户错误页本福斯特