如何在ASP.NET MVC中正确处理404错误?

446

我正在使用RC2版本。

使用URL路由:

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

以上似乎已经处理了这样的请求(假设在最初的 MVC 项目中设置了默认路由表):"/blah/blah/blah/blah"

在控制器本身中覆盖 HandleUnknownAction() 方法:

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

然而,之前的方法无法处理对错误/未知控制器的请求。例如,如果我没有“/IDoNotExist”,但我请求它,我将从Web服务器获得通用的404页面,而不是使用路由+覆盖后的自定义404页面。

因此,我的问题是:是否有任何方法可以在MVC框架本身中使用路由或其他方式捕获此类型的请求?

或者,我应该默认使用Web.Config customErrors作为我的404处理程序,然后忘记所有这些?我假设如果我使用customErrors,由于Web.Config对直接访问的限制,我将不得不将通用的404页面存储在/Views之外。


3
这是一个404错误,我建议你不必过多关注它。让它显示404页面,因为用户很可能输入了错误的内容,或者如果某个页面已经移动了,你的应用程序应该将该请求进行永久重定向。 404错误属于web服务器而不是应用程序。你可以随时自定义IIS错误页面。 - mamu
你也可以看看这个解决方案:http://blog.dantup.com/2009/04/aspnet-mvc-handleerror-attribute-custom.html - NoWar
http://ben.onfabrik.com/posts/aspnet-mvc-custom-error-pages 这篇文章也提供了一些很好的信息。 - Chris S
4
很遗憾,虽然经过了四个稳定版本和五年多的时间,但处理 asp.net MVC + IIS 中的404错误的情况仍然没有得到改善,这仍然是解决该问题的常见问答。 - joelmdev
19个回答

279
代码取自http://blogs.microsoft.co.il/blogs/shay/archive/2009/03/06/real-world-error-hadnling-in-asp-net-mvc-rc2.aspx,同样适用于ASP.net MVC 1.0版本。

以下是我处理http异常的方法:

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));
}

23
更新:检查 HTTP 404 错误绝对是必要的,但我仍不确定何时会出现 500 错误。此外,您还需要明确设置 Response.StatusCode = 404 或 500,否则如果您返回 200 状态码(当前代码正在执行的操作),Google 将开始索引这些页面。 - Simon_Weaver
6
@Simon_Weaver: 同意!这需要返回404状态代码。只有在这样做之后,它才能真正作为404解决方案。请查看:http://www.codinghorror.com/blog/2007/03/creating-user-friendly-404-pages.html - Matt Kocaj
3
正如上面的评论和链接帖子所提到的,这似乎行不通。错误控制器被触发,但返回空白屏幕。(使用MVC 3) - RyanW
4
感觉有些不对劲,整个MVC的目的就是为了消除所有这些抽象,但这里又出现了… - Alex Nolasco
1
这只有潜力成为“Application_Error”仅在出现错误时运行的请求的解决方案,这意味着A)通过ASP.NET管道运行的请求,并且B)不会在asp.net生命周期的早期遇到错误,比如Web.config无效,或者设置路由时出错。这个问题是关于404的,但解决方案包括其他错误,所以请注意,这远非完整的解决方案。此外,因为您使用的是控制器和视图而不是静态文件,即使它运行,错误处理代码也可能产生错误。 - wired_in
显示剩余9条评论

258

404要求

以下是我对于404解决方案的要求,并展示了如何实现:

  • 匹配路由时,处理错误操作
  • 匹配路由时,处理错误控制器
  • 处理未匹配的路由(应用程序无法识别的任意URL) - 我不希望它们上升到Global.asax或IIS,因为我无法正确地重定向回我的MVC应用程序
  • 以与上述相同的方式处理自定义404 - 例如,当提交不存在的对象ID(可能已删除)
  • 所有的404都必须返回一个MVC视图(而不是静态页面),以后如果需要可以向其中添加更多数据(好的404设计)并且必须返回HTTP 404状态码。

解决方案

我认为你应该将Application_Error保存在Global.asax中,用于更高级的事情,比如未处理的异常和日志记录(正如Shay Jacoby的答案所示),但不适用于404处理。这就是为什么我的建议将404处理从Global.asax文件中分离出来。

步骤1:为404错误逻辑找到一个共同的地方

这对于可维护性是个好主意。使用一个ErrorController,以便将来改进你设计良好的404页面时可以轻松适应。同时,确保你的响应返回404状态码

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
}

步骤二:使用基础控制器类,以便轻松调用自定义的404操作并连接HandleUnknownAction

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

InvokeHttp404方法为重定向到ErrorController和我们的新Http404操作创建了一个通用的地方。想想DRY

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

像这样(不一定要使用StructureMap):

MVC1.0示例:

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示例:

    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:在Global.asax中添加一个NotFound路由,用于解析失败的URL到你的应用程序中

此路由应指向我们的Http404操作。注意,url参数将是相对URL,因为路由引擎在此处剥离了域部分?这就是为什么我们在第一步中有所有条件URL逻辑的原因。

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

这是 MVC 应用程序中第三个也是最后一个捕获404的位置,如果您没有在此处捕获不匹配的路由,则 MVC 将将问题传递给 ASP.NET(Global.asax),在此情况下您并不希望出现这种情况。

步骤5:最后,在您的应用程序找不到某些内容时调用404

例如,当错误的ID提交到我的 Loans控制器 (派生自MyController)时:

    //
    // 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);
    }

如果所有这些内容都可以在更少的地方用更少的代码连接起来,那就太好了,但我认为这个解决方案更易于维护、更易于测试和相当实用。

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

注意:这个答案已经从原先的回答中进行了重大修改,但目的/要求是相同的——这就是为什么我没有添加新的回答的原因。


12
感谢提供完整的写作。另外需要补充的是,在IIS7下运行时,您需要将属性“TrySkipIisCustomErrors”设置为true。否则,IIS仍将返回默认的404页面。 我们在第5步设置状态码后添加了Response.TrySkipIiisCustomErrors=true;。http://msdn.microsoft.com/en-us/library/system.web.httpresponse.tryskipiiscustomerrors.aspx - Rick
8
这对 MVC3 运作良好。我将 ObjectFactory.GetInstance 替换为更通用的 MVC3 的 DependencyResolver.Current.GetService。我正在使用 Ninject。 - kamranicus
126
还有其他人觉得在Web框架中,404错误这样一个常见的东西竟然如此复杂就显得非常荒谬吗? - quentin-starin
1
似乎对于任何以“.cshtml”结尾的错误网址,我也会得到IIS 404页面。 - quentin-starin
1
是的,但你说了新的方法。这个帖子中的所有帖子都很旧了。我以为你指的是一个特定的帖子/文章。 - PussInBoots
显示剩余57条评论

241

ASP.NET MVC不太支持自定义404页面。使用自定义控制器工厂、捕获所有路由、基础控制器类中的HandleUnknownAction等方式都很麻烦!

IIS自定义错误页面是一个更好的替代方案:

web.config

<system.webServer>
  <httpErrors errorMode="Custom" existingResponse="Replace">
    <remove statusCode="404" />
    <error statusCode="404" responseMode="ExecuteURL" path="/Error/PageNotFound" />
  </httpErrors>
</system.webServer>

ErrorController

public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View();
    }
}

示例项目


40
这应该是被接受的答案!!! 在使用IIS Express的ASP.NET MVC 3上运行非常出色。 - Andrei Rînea
7
如果你正在使用IIS7+,那么这绝对是正确的方法。加一! - elo80ka
3
在同一项目中使用 JSON,是否可能仅返回状态码 404? - VinnyG
6
在IIS Express上运行非常好,但是一旦我将网站部署到生产环境的IIS 7.5中,我只能看到一个白色页面而不是错误视图。 - Moulde
2
根据我的测试(使用MVC3),这会破坏 customErrors mode="On"HandleErrorAttribute同时运作的功能。控制器操作中未处理的异常的自定义错误页面不再提供服务。 - Slauma
显示剩余10条评论

154
快速答案/简而言之

enter image description here

对于那些懒惰的人:

Install-Package MagicalUnicornMvcErrorToolkit -Version 1.0

然后从 global.asax 中删除此行。

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

这仅适用于IIS7+和IIS Express。

如果您正在使用Cassini……那么……嗯……尴尬…… awkward


长而详细的回答

我知道这个问题已经有了答案。但是答案非常简单(感谢David FowlerDamian Edwards真正回答了这个问题)。

不需要进行任何自定义操作。

对于ASP.NET MVC3,所有的部分都已经准备好了。

步骤1 -> 在两个位置更新你的web.config文件。

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

并且

<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>

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

  • /NotFound <- 用于404未找到错误页面。
  • /ServerError <- 用于任何其他错误,包括发生在我的代码中的错误。这是500个内部服务器错误

看看<system.web>中的第一部分只有一个自定义条目吗? statusCode="404" 条目?我只列出了一个状态码,因为所有其他错误,包括500 Server Error(即当您的代码有错误并崩溃用户请求时发生的烦人错误)...所有其他错误都由设置 defaultRedirect="/ServerError" 处理...这意味着,如果您不是404页面未找到,则请转到路由/ServerError

好的。那就没问题了.. 现在来看看我在global.asax中列出的路由

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

这是我的完整路由部分...

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}
        );
}

那里列出了两个忽略路由,即axdfavicons(哦!对您来说还有额外的忽略路由!)然后(顺序很重要),我有两个显式的错误处理路由...然后是任何其他路由。在这种情况下,默认路由。当然,我还有更多,但那是特定于我的网站的。确保错误路由位于列表顶部。顺序至关重要。

最后,在我们的global.asax文件中,我们不会全局注册HandleError属性。不,不,先生。不行。否定的。不......

global.asax中删除此行。

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

步骤三 - 创建具有操作方法的控制器

现在,我们添加一个带有两个操作方法的控制器...

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完美地配合使用。Elmah真是太好用了!

朋友们,就是这样了。

现在,恭喜您读到这么多,并获得一只独角兽作为奖励!

enter image description here


3
抱歉,对我来说任何改变404的URL的解决方案都是错误的。在MVC中,使用WebConfig没有办法在不更改URL的情况下处理它,除非您需要创建静态HTML文件或aspx(是的,就是普通的aspx文件)。如果您愿意在URL中包含“?aspxerrorpath=/er/not/found”,那么您的解决方案就可以了。 - Gutek
8
这可能听起来很奇怪 - 但我的答案早就给出了,我同意你的@Gutek,我不喜欢再重定向到错误页面了。我曾经这么做过(参考我的答案 :P)。如果错误发生在 /some/resource 上.. 那么该资源应返回 404 或 500 等错误代码。否则会对搜索引擎优化造成巨大影响。啊.. 时代变了 :) - Pure.Krome
你的链接已经失效了...别担心,我不使用Cassini。 - Chris
1
@Chris <在此插入你最喜欢的神明> 该死。我现在甚至都记不起来是什么了。好吧,我的模因收藏来拯救...然后..问题解决了。 - Pure.Krome
由于解决方案中存在巨大的干扰,被投下了反对票。 - GaTechThomas
显示剩余2条评论

90

我在MVC(具体来说是MVC3)中调查了很多关于如何正确处理404的信息,而这个方法,依我的观点,是我找到的最好解决方案:

在global.asax中:

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));
        }
    }
}

错误控制器:

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;
    }
}

(可选)

解释:

据我所知,ASP.NET MVC3应用程序可以产生6种不同的404情况。

(由ASP.NET Framework自动生成:)

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

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

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

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

(手动生成:)

(4) 操作使用方法HttpNotFound()返回HttpNotFoundResult。

(5) 操作引发具有状态代码404的HttpException。

(6) 操作手动修改Response.StatusCode属性为404。

通常,您想要实现3个目标:

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

(2) 在客户端响应中保持404状态代码(对于SEO特别重要)。

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

有多种方法尝试实现此目标:

(1)

<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customError>
</system.web>

这种解决方案存在的问题:

  1. 在情况(1)、(4)、(6)中,不符合目标(1)。
  2. 不能自动符合目标(2),必须手动编程。
  3. 不符合目标(3)。

(2)

<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

这种解决方案存在的问题:

  1. 仅适用于IIS 7+。
  2. 在情况(2)、(3)、(5)中不符合目标(1)。
  3. 不会自动符合目标(2),必须手动编程。

(3)

<system.webServer>
    <httpErrors errorMode="Custom" existingResponse="Replace">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

此解决方案存在的问题:

  1. 仅适用于IIS 7+。
  2. 不符合目标(2)的自动要求,必须手动编程。
  3. 它模糊了应用程序级别的http异常。例如,无法使用customErrors部分、System.Web.Mvc.HandleErrorAttribute等。它只能显示通用错误页面。

(4)

<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customError>
</system.web>

<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

这种解决方案存在的问题:

  1. 仅适用于IIS 7+。
  2. 不能自动满足目标(2),必须手动编程。
  3. 在情况(2)、(3)、(5)中不符合目标(3)。

有些人之前遇到过类似问题,甚至尝试创建自己的库(请参见http://aboutcode.net/2011/02/26/handling-not-found-with-asp-net-mvc3.html)。但是前面提供的解决方案似乎涵盖了所有情况,而不需要使用外部库的复杂性。


7
谢谢!这不能在Application_Error下完成,因为从控制器抛出的显式404不被视为ASP.NET上的错误。 如果你从一个控制器返回HttpNotFound(),Application_Error事件将永远不会触发。 - Marco
1
我认为您在ErrorsController中忘记了public ActionResult NotFound() {}。另外,您能否解释一下针对AJAX请求,您的_NotFound部分视图会是什么样子? - d4n3
3
在MVC 4中,我总是在c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));这一行上收到MissingMethodException: Cannot create an abstract class的错误。有任何想法吗? - µBio
1
如果“未找到”的URL路径中包含一个点(例如http://example.com/hi.bob),那么Application_EndRequest根本不会触发,我会得到IE的通用404页面。 - Bob.at.Indigo.Health
@µBio 确保你在使用 System.Web.Routing.RequestContext。我曾遇到同样的错误,它试图使用 System.ServiceModel.Channels.RequestContext。 - madbrendon
显示剩余3条评论

14

我非常喜欢cottsaks的解决方案,并且认为它解释得非常清楚。我的唯一补充是修改步骤2如下:

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触发异常处理程序两次。例如对于诸如asdfsdf/dfgdfgd之类的url。


4
这太好了。那些“两次”的情况开始让我感到困扰。我更新了我的答案 - Matt Kocaj
如果用户输入错误的控制器和操作名称,上述解决方案是否有效? - Monojit Sarkar

7
我只能让@cottsak的方法适用于无效控制器的方式是修改CustomControllerFactory中现有的路由请求,如下所示:
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。


你知道为什么吗?(特指MVC2?) - Matt Kocaj
我认为关键是修改现有的请求而不是创建一个新的,但我之前做过这件事,所以我不确定那是否正确。从控制器工厂中调用“InvokeHttp404”没有起作用。 - Dave K
我今天更新了我的答案,加入了一些MVC2的具体内容。请问您能否告诉我,我上面详细说明的解决方案是否仍然对您无效? - Matt Kocaj

5
这里有另一种使用MVC工具的方法,您可以在Action方法内处理对于错误控制器名称、错误路由名称和任何其他标准的请求。个人而言,我更喜欢尽可能避免使用web.config设置,因为它们会执行302/200重定向并不支持使用Razor视图的ResponseRewrite (Server.Transfer)。基于SEO原因,我更愿意返回一个自定义错误页面的404错误。
其中一些是对以上cottsak技术的新尝试。
此解决方案还使用了最少量的web.config设置,更倾向于使用MVC 3错误过滤器。
用法:
只需从动作或自定义ActionFilterAttribute抛出HttpException即可。
Throw New HttpException(HttpStatusCode.NotFound, "[Custom Exception Message Here]")

步骤1

将以下设置添加到您的web.config文件中。这是使用MVC的HandleErrorAttribute所必需的。

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

步骤2

添加一个类似于MVC框架的HandleErrorAttribute的自定义HandleHttpErrorAttribute,但是用于HTTP错误:

<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

步骤三

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

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"})

步骤四

创建一个基础控制器类,并在您的控制器中继承它。这一步允许我们处理未知的操作名称并将HTTP 404错误提供给我们的HandleHttpErrorAttribute。

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

步骤五

创建一个ControllerFactory重写,在Application_Start中重写它。这一步允许我们在指定无效的控制器名称时引发HTTP 404异常。

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

在你的RoutTable.Routes中包含一个特殊的路由,用于BaseController Unknown action。这将帮助我们在用户访问未知控制器或未知操作时引发404错误。

'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消息:

/InvalidController
/Home/InvalidRoute
/InvalidController/InvalidRoute
/Home/TriggerNotFound

cottsak上面的帖子以及这些文章都是很好的参考。


嗯,我无法让它工作:IControllerFactory 'aaa.bbb.CustomControllerFactory'未返回名称为'123'的控制器。 - 你有什么想法为什么会出现这种情况? - enashnash
redirectMode="ResponseRedirect"。这将返回一个302 Found + 200 OK,这对于SEO来说并不好! - PussInBoots

5

我的简化解决方案适用于未处理的区域、控制器和操作:

  1. Create a view 404.cshtml.

  2. Create a base class for your controllers:

    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");
        }
    }
    
  3. Create a custom controller factory returning your base controller as a fallback:

    public class ControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType != null)
                return base.GetControllerInstance(requestContext, controllerType);
    
            return new Controller();
        }
    }
    
  4. Add to Application_Start() the following line:

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

4
在MVC4 WebAPI中,可以通过以下方式处理404错误:
COURSES APICONTROLLER
    // 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;
    }

主页控制器

public ActionResult Course(int id)
{
    return View(id);
}

视图

<div id="course"></div>
<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!');    
            }
        }
    });
</script>

全局

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 }
    );
}

结果

这里输入图片描述


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接