Как я могу правильно обрабатывать 404 в ASP.NET MVC?

Я использую RC2

Использование маршрутизации URL:

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

Вышеприведенное, кажется, заботится о таких запросах (при условии, что таблицы маршрутизации по умолчанию установлены первоначальным проектом MVC): "/ бла / бла / бла / бла"

Переопределение HandleUnknownAction() в самом контроллере:

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

Однако предыдущие стратегии не обрабатывают запрос к плохому / неизвестному контроллеру. Например, у меня нет "/IDoNotExist", если я запрашиваю это, я получаю общую страницу 404 с веб-сервера, а не мою 404, если я использую маршрутизацию + переопределение.

Итак, наконец, мой вопрос: есть ли способ перехватить этот тип запроса, используя маршрут или что-то еще в самой структуре MVC?

ИЛИ мне просто по умолчанию использовать Web.Config customErrors в качестве моего обработчика 404 и забыть все это? Я предполагаю, что если я пойду с customErrors, мне придется хранить общую страницу 404 вне /Views из-за ограничений Web.Config на прямой доступ.

19 ответов

Решение

Код взят из 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));
}

Требования к 404

Ниже приведены мои требования к решению 404, и ниже я покажу, как я его реализую:

  • Я хочу обработать совпавшие маршруты с плохими действиями
  • Я хочу обрабатывать совпадающие маршруты с плохими контроллерами
  • Я хочу обрабатывать несопоставленные маршруты (произвольные URL, которые мое приложение не может понять) - я не хочу, чтобы они пересекались с Global.asax или IIS, потому что тогда я не могу правильно перенаправить обратно в свое приложение MVC
  • Я хочу способ обработки таким же образом, как указано выше, пользовательские 404 - например, когда передается идентификатор для несуществующего объекта (может быть удален)
  • Я хочу, чтобы все мои 404 вернули представление MVC (не статическую страницу), на которое я могу при необходимости добавить больше данных (при хорошем дизайне 404), и они должны вернуть код состояния HTTP 404

Решение

Я думаю, что вы должны сохранить Application_Error в Global.asax для более высоких вещей, таких как необработанные исключения и ведение журнала (как показано в ответе Шей Джейкоби), но не обработка 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
}

Шаг 2: Используйте базовый класс Controller, чтобы вы могли легко вызывать собственное действие 404 и подключаться HandleUnknownAction

404-е в ASP.NET MVC нужно ловить в нескольких местах. Первый HandleUnknownAction,

InvokeHttp404 Метод создает общее место для перенаправления на ErrorController и наш новый Http404 действие. Думай СУХОЙ!

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: Используйте Dependency Injection на фабрике контроллеров и подключите 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. Добавьте NotFound маршрут в Global.asax для URL, которые не могут быть проанализированы в вашем приложении

Этот маршрут должен указывать на наш Http404 действие. Обратите внимание на url param будет относительным URL, потому что механизм маршрутизации удаляет здесь доменную часть? Вот почему у нас есть вся эта условная логика URL на шаге 1.

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

Это третье и последнее место, где можно поймать 404-е в приложении MVC, которое вы сами не вызываете. Если вы не поймаете несопоставленные маршруты здесь, то MVC передаст проблему в ASP.NET (Global.asax), и вы действительно не хотите этого в этой ситуации.

Шаг 5: Наконец, вызовите 404 s, когда ваше приложение не может найти что-то

Например, когда мой контролер 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);
    }

Было бы неплохо, если бы все это можно было подключить в меньшем количестве мест с меньшим количеством кода, но я думаю, что это решение более легко обслуживаемо, более тестируемо и довольно прагматично.

Спасибо за отзыв до сих пор. Я хотел бы получить больше.

ПРИМЕЧАНИЕ: это было значительно отредактировано из моего первоначального ответа, но цель / требования те же - поэтому я не добавил новый ответ

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

Пример проекта

Быстрый ответ / TL;DR

Для ленивых людей там:

Install-Package MagicalUnicornMvcErrorToolkit -Version 1.0

Затем удалите эту строку из global.asax

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

И это только для IIS7+ и IIS Express.

Если вы используете Кассини.. ну.. эм.. эээ... неловко... неловко


Долго объяснил ответ

Я знаю, что на это ответили. Но ответ ДЕЙСТВИТЕЛЬНО ПРОСТО (приветствует davidfowl и Damian 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}
        );
}

Это список двух игнорируемых маршрутов -> axd's а также favicons (ооо! бонус игнорировать маршрут, для вас!) Затем (и порядок здесь ИМПЕРАТИВНЫЙ), у меня есть два явных маршрута обработки ошибок..., за которыми следуют любые другие маршруты. В этом случае по умолчанию. Конечно, у меня есть больше, но это особенное для моего веб-сайта. Просто убедитесь, что маршруты ошибок находятся вверху списка. Порядок обязателен.

Наконец, пока мы внутри global.asax файл, мы НЕ регистрируем глобально атрибут HandleError. Нет, нет, нет, сэр. Nadda. Нету. Nien. Negative. Noooooooooo...

Удалить эту строку из global.asax

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

Шаг 3 - Создайте контроллер с методами действия

Теперь.. мы добавляем контроллер с двумя методами действия...

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 - Создание представлений

И наконец, создайте два представления. Поместите их в обычное место просмотра для этого контроллера.

Бонусные комментарии

  • Вам не нужно Application_Error(object sender, EventArgs e)
  • Все вышеперечисленные шаги прекрасно работают с Elmah. Elmah Fraking Wroxs!

И это, друзья мои, должно быть так.

Теперь, поздравляю с этим чтением и получаю единорога в качестве приза!

Я исследовал много о том, как правильно управлять 404 с в MVC (особенно MVC3), и это, IMHO, лучшее решение, которое я придумал:

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

ErrorsController:

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 Framework:)

(1) URL не находит соответствия в таблице маршрутов.

(Автоматически генерируется ASP.NET MVC Framework:)

(2) URL находит совпадение в таблице маршрутов, но указывает несуществующий контроллер.

(3) URL находит совпадение в таблице маршрутов, но указывает несуществующее действие.

(Генерируется вручную:)

(4) Действие возвращает HttpNotFoundResult с помощью метода HttpNotFound ().

(5) Действие выдает HttpException с кодом состояния 404.

(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) в случаях (1), (4), (6).
  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. Не соответствует цели (1) в случаях (2), (3), (5).
  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. Не соответствует цели (3) в случаях (2), (3), (5).

Люди, которые раньше сталкивались с этим, даже пытались создать свои собственные библиотеки (см. http://aboutcode.net/2011/02/26/handling-not-found-with-asp-net-mvc3.html). Но предыдущее решение, кажется, охватывает все случаи без сложности использования внешней библиотеки.

Мне действительно нравится решение коттсаков, и я думаю, что оно очень четко объяснено. мое единственное добавление было изменить шаг 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-адреса, содержащие недопустимые действия И контроллеры, от запуска процедуры исключения дважды. например, для URL, таких как asdfsdf/dfgdfgd

Единственный способ заставить метод @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.

Мое сокращенное решение, которое работает с необработанными областями, контроллерами и действиями:

  1. Создайте представление 404.cshtml.

  2. Создайте базовый класс для ваших контроллеров:

    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. Создайте фабрику пользовательских контроллеров, возвращающую базовый контроллер как запасной вариант:

    public class ControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType != null)
                return base.GetControllerInstance(requestContext, controllerType);
    
            return new Controller();
        }
    }
    
  4. добавить в Application_Start() следующая строка:

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

Вот еще один метод, использующий инструменты MVC, с помощью которого вы можете обрабатывать запросы к неверным именам контроллеров, неверным именам маршрутов и любым другим критериям, которые вы считаете подходящими внутри метода Action. Лично я предпочитаю избегать как можно большего числа настроек web.config, потому что они выполняют перенаправление 302 / 200 и не поддерживают ResponseRewrite (Server.Transfer) используя виды Razor. Я предпочел бы вернуть 404 с пользовательской страницей ошибок по причинам SEO.

Отчасти это новый взгляд на технику Котцака выше.

Это решение также использует минимальные настройки web.config вместо фильтров ошибок MVC 3.

использование

Просто сгенерируйте исключение HttpException из действия или пользовательского ActionFilterAttribute.

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

Шаг 1

Добавьте следующий параметр в ваш файл web.config. Это необходимо для использования HandleErrorAttribute MVC.

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

Шаг 2

Добавьте пользовательский атрибут HandleHttpErrorAttribute, аналогичный атрибуту HandleErrorAttribute инфраструктуры MVC, за исключением ошибок 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

Шаг 3

Добавить фильтры в GlobalFilterCollection (GlobalFilters.Filters) в Global.asax, В этом примере все ошибки 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"})

Шаг 4

Создайте базовый класс контроллеров и наследуйте его от своих контроллеров. Этот шаг позволяет нам обрабатывать неизвестные имена действий и выдавать ошибку 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

Шаг 5

Создайте переопределение ControllerFactory и переопределите его в файле Global.asax в 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. Это поможет нам поднять 404 в случае, когда пользователь получает доступ к неизвестному контроллеру или неизвестному действию.

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

Резюме

Этот пример продемонстрировал, как можно использовать инфраструктуру MVC для возврата 404 Http-кодов ошибок в браузер без перенаправления с использованием атрибутов фильтра и общих представлений об ошибках. Он также демонстрирует отображение той же пользовательской страницы ошибок, когда указаны недопустимые имена контроллеров и имена действий.

Я добавлю скриншот недопустимого имени контроллера, имени действия и пользовательского 404, созданного в результате действия Home/TriggerNotFound, если я наберу достаточно голосов, чтобы опубликовать один =). Fiddler возвращает сообщение 404, когда я получаю доступ к следующим URL-адресам, используя это решение:

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

Пост Котцака выше и эти статьи были хорошими ссылками.

В MVC4 WebAPI 404 может обрабатываться следующим образом,

КУРСЫ 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 }
    );
}

РЕЗУЛЬТАТЫ

Работа с ошибками в ASP.NET MVC - это просто боль в заднице. Я перепробовал много предложений на этой странице и на других вопросах и сайтах, и ничего не работает хорошо. Одно из предложений заключалось в обработке ошибок в файле web.config внутри system.webserver, но он просто возвращает пустые страницы.

Моя цель при разработке этого решения заключалась в том, чтобы:

  • НЕ НАПРАВЛЕНО
  • Вернуть PROPER STATUS CODES не 200/Ok, как при обработке ошибок по умолчанию

Вот мое решение.

1. Добавьте следующее в раздел system.web

   <system.web>
     <customErrors mode="On" redirectMode="ResponseRewrite">
      <error statusCode="404"  redirect="~/Error/404.aspx" />
      <error statusCode="500" redirect="~/Error/500.aspx" />
     </customErrors>
    <system.web>

Выше обрабатываются любые URL-адреса, не обработанные route.config, и необработанные исключения, особенно те, которые встречаются в представлениях. Обратите внимание, что я использовал aspx, а не html. Это так, я могу добавить код ответа на код позади.

2 Создайте папку с именем Error (или любую другую) в корне вашего проекта и добавьте две веб-формы. Ниже моя страница 404;

<%@ 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</title>
    <link href="<%=ResolveUrl("~/Content/myapp.css")%>" rel="stylesheet" />
</head>
<body>
    <div class="top-nav">
      <a runat="server" class="company-logo" href="~/"></a>
    </div>
    <div>
        <h1>404 - Page Not found</h1>
        <p>The page you are looking for cannot be found.</p>
        <hr />
        <footer></footer>
    </div>
</body>
</html>

И на код позади я установил код ответа

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

Сделайте то же самое для 500 страниц

3. Для обработки ошибок в контроллерах. Есть много способов сделать это. Это то, что сработало для меня. Все мои контроллеры наследуются от базового контроллера. В базовом контроллере у меня есть следующие методы

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. Добавьте CustomError.cshtml в папку " Общие представления". Ниже мое;

<h1>@ViewBag.Title</h1>
<br />
<p>@ViewBag.Message</p>

Теперь в вашем контроллере приложения вы можете сделать что-то вроде этого;

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 другим способом.

Мне кажется, что стандарт CustomErrors Конфигурация должна работать, однако, из-за зависимости от Server.Transfer кажется, что внутренняя реализация ResponseRewrite не совместим с MVC.

Для меня это похоже на явную дыру в функциональности, поэтому я решил повторно реализовать эту функцию с помощью модуля HTTP. Приведенное ниже решение позволяет вам обрабатывать любой код состояния HTTP (включая 404) путем перенаправления на любой действительный маршрут MVC, как вы это обычно делаете.

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

Это было проверено на следующих платформах;

  • MVC4 в режиме интегрированного конвейера (IIS Express 8)
  • MVC4 в классическом режиме (VS Development Server, Cassini)
  • MVC4 в классическом режиме (IIS6)

Выгоды

  • Универсальное решение, которое можно добавить в любой проект MVC
  • Включает поддержку традиционной пользовательской конфигурации ошибок
  • Работает в режимах Integrated Pipeline и Classic

Решение

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() {

        }


    }

}

использование

Включите это как последний HTTP-модуль в ваш web.config

  <system.web>
    <httpModules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </httpModules>
  </system.web>

  <!-- IIS7+ -->
  <system.webServer>
    <modules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </modules>
  </system.webServer>

Для тех из вас, кто обратил внимание, вы заметите, что в режиме Integrated Pipeline это всегда будет отвечать HTTP 200 из-за способа Server.TransferRequest работает. Чтобы вернуть правильный код ошибки, я использую следующий контроллер ошибок.

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

}

Попробуйте NotFoundMVC на nuget. Работает, без настроек.

Мое решение, если кто-то найдет его полезным.

В Web.config:

<system.web>
    <customErrors mode="On" defaultRedirect="Error" >
      <error statusCode="404" redirect="~/Error/PageNotFound"/>
    </customErrors>
    ...
</system.web>

В Controllers/ErrorController.cs:

public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        if(Request.IsAjaxRequest()) {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return Content("Not Found", "text/plain");
        }

        return View();
    }
}

Добавить PageNotFound.cshtml в Shared папка, и все тут.

Публикация ответа, так как мой комментарий был слишком длинным...

Это и комментарий, и вопросы к сообщению / ответу единорога:

/questions/15987362/kak-ya-mogu-pravilno-obrabatyivat-404-v-aspnet-mvc/15987368#15987368

Я предпочитаю этот ответ другим за его простоту и тот факт, что, по-видимому, с некоторыми людьми в Microsoft консультировались. Однако я получил три вопроса, и если на них можно будет ответить, я назову этот ответ святым Граалем всех ответов об ошибках 404/500 на веб-страницах для приложения ASP.NET MVC (x).

@ Pure.Krome

  1. Можете ли вы обновить свой ответ с помощью SEO-материалов из комментариев, указанных GWB (об этом никогда не упоминалось в вашем ответе) - <customErrors mode="On" redirectMode="ResponseRewrite"> а также <httpErrors errorMode="Custom" existingResponse="Replace">?

  2. Можете ли вы спросить своих друзей из команды ASP.NET, нормально ли это делать - было бы неплохо получить какое-то подтверждение - может быть, это большое нет-нет, чтобы изменить redirectMode а также existingResponse таким образом, чтобы иметь возможность хорошо играть с SEO?!

  3. Можете ли вы добавить некоторые разъяснения, касающиеся всего этого (customErrors redirectMode="ResponseRewrite", customErrors redirectMode="ResponseRedirect", httpErrors errorMode="Custom" existingResponse="Replace", УДАЛИТЬ customErrors ПОЛНОСТЬЮ, как кто-то предложил) после общения с друзьями в Microsoft?

Как я говорил; было бы здорово, если бы мы могли сделать ваш ответ более полным, так как это кажется довольно популярным вопросом с более чем 54 000 просмотров.

Обновление: ответ Unicorn делает 302 Найденных и 200 OK и не может быть изменен, чтобы только возвратить 404, используя маршрут. Это должен быть физический файл, который не очень MVC: иш. Так что переходим к другому решению. Жаль, потому что это, казалось, было окончательным ответом MVC: так далеко.

1) Сделать абстрактный класс Controller.

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) Сделайте наследование от этого абстрактного класса во всех ваших контроллерах

public class HomeController : MyController
{}  

3) И добавьте представление с именем "NotFound" в папку View-Shared.

Добавление моего решения, которое почти идентично решению Германа Кана, с небольшой складкой, чтобы оно работало для моего проекта.

Создайте собственный контроллер ошибок:

public class Error404Controller : BaseController
{
    [HttpGet]
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View("404");
    }
}

Затем создайте фабрику пользовательских контроллеров:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        return controllerType == null ? new Error404Controller() : base.GetControllerInstance(requestContext, controllerType);
    }
}

Наконец, добавьте переопределение к пользовательскому контроллеру ошибок:

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.

Я просмотрел все статьи, но у меня ничего не работает: мое требование пользователя должно что-то указывать на вашей странице 404 URL-адреса. Я подумал, что это очень просто. Но вы должны правильно понимать обработку 404:

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

Я нашел эту статью очень полезной. Должен быть прочитан сразу. Обычная ошибка страницы-Бен Фостер

Я просмотрел большинство решений, опубликованных в этой теме. Хотя этот вопрос может быть старым, он все еще очень применим к новым проектам даже сейчас, поэтому я потратил довольно много времени на чтение ответов, представленных здесь, а также где-либо еще.

Как @Marco указал на различные случаи, при которых может произойти 404, я проверил решение, которое я собрал вместе, с этим списком. Помимо его списка требований я также добавил еще один.

  • Решение должно иметь возможность обрабатывать вызовы MVC и AJAX/WebAPI наиболее подходящим способом. (т. е. если 404 происходит в MVC, на нем должна отображаться страница "Не найдено", а если 404 происходит в WebAPI, он не должен перехватывать ответ XML/JSON, чтобы потребляющий Javascript мог легко его проанализировать).

Это решение в 2 раза:

Первая часть написана @Guillaume по адресу /questions/4074101/customerrors-protiv-httperrors-znachitelnyij-nedostatok-dizajna/4074109#4074109. Их решение заботится о любых 404, которые были вызваны из-за неверного маршрута, неверного контроллера и недопустимого действия.

Идея состоит в том, чтобы создать WebForm, а затем заставить его вызывать действие Not Found вашего контроллера ошибок MVC. Он делает все это без какого-либо перенаправления, поэтому вы не увидите ни одного 302 в Fiddler. Оригинальный URL также сохраняется, что делает это решение фантастическим!


Вторая часть написана @Germán по адресу /questions/29759081/kak-vernut-predstavlenie-dlya-httpnotfound-v-aspnet-mvc-3/29759108#29759108. Их решение заботится о любых 404, возвращаемых вашими действиями в форме HttpNotFoundResult() или сгенерировать новое HttpException()!

Идея состоит в том, чтобы фильтр посмотрел на ответ, а также на исключение, выданное вашими контроллерами MVC, и вызвал соответствующее действие в вашем контроллере ошибок. Опять же, это решение работает без какого-либо перенаправления и оригинальный URL сохраняется!


Как видите, оба этих решения вместе предлагают очень надежный механизм обработки ошибок, и они отвечают всем требованиям, указанным @Marco, а также моим требованиям. Если вы хотите увидеть рабочий образец или демонстрацию этого решения, пожалуйста, оставьте в комментариях, и я был бы рад собрать его вместе.

Другие вопросы по тегам