Как правильно отправить ответ HTTP 404 от действия ASP.NET MVC?

92

Если указан маршрут:

{FeedName} / {ItemPermalink}

пример: / Блог / Hello-World

Если элемент не существует, я хочу вернуть 404. Как правильно сделать это в ASP.NET MVC?

Даниэль Шаффер
источник
Кстати, спасибо, что задали этот вопрос. Это происходит в моих стандартных дополнениях к проекту: D
Эрик ван Бракель

Ответы:

69

Стрельба от бедра (ковбойское кодирование ;-)), я бы предложил примерно следующее:

Контроллер:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return new HttpNotFoundResult("This doesn't exist");
    }
}

HttpNotFoundResult:

using System;
using System.Net;
using System.Web;
using System.Web.Mvc;

namespace YourNamespaceHere
{
    /// <summary>An implementation of <see cref="ActionResult" /> that throws an <see cref="HttpException" />.</summary>
    public class HttpNotFoundResult : ActionResult
    {
        /// <summary>Initializes a new instance of <see cref="HttpNotFoundResult" /> with the specified <paramref name="message"/>.</summary>
        /// <param name="message"></param>
        public HttpNotFoundResult(String message)
        {
            this.Message = message;
        }

        /// <summary>Initializes a new instance of <see cref="HttpNotFoundResult" /> with an empty message.</summary>
        public HttpNotFoundResult()
            : this(String.Empty) { }

        /// <summary>Gets or sets the message that will be passed to the thrown <see cref="HttpException" />.</summary>
        public String Message { get; set; }

        /// <summary>Overrides the base <see cref="ActionResult.ExecuteResult" /> functionality to throw an <see cref="HttpException" />.</summary>
        public override void ExecuteResult(ControllerContext context)
        {
            throw new HttpException((Int32)HttpStatusCode.NotFound, this.Message);
        }
    }
}
// By Erik van Brakel, with edits from Daniel Schaffer :)

Используя этот подход, вы соблюдаете рамочные стандарты. Там уже есть HttpUnauthorizedResult, так что это просто расширит фреймворк в глазах другого разработчика, поддерживающего ваш код позже (вы знаете, псих, который знает, где вы живете).

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


Я только что использовал отражатель, чтобы взглянуть на HttpUnauthorizedResult. Кажется, они устанавливают StatusCode в ответе на 0x191 (401). Хотя это работает для 401, используя 404 в качестве нового значения, я, кажется, получаю просто пустую страницу в Firefox. Internet Explorer показывает по умолчанию 404 (не версию ASP.NET). Используя панель инструментов веб-разработчика, я проверил заголовки в FF, которые ДОЛЖНЫ отображать ответ 404 Not Found. Возможно, я просто неправильно настроил FF.


При этом я думаю, что подход Джеффа является прекрасным примером KISS. Если вам не нужна многословность в этом примере, его метод тоже подойдет.

Эрик ван Бракель
источник
Да, я тоже заметил Enum. Как я уже сказал, это всего лишь грубый пример, не стесняйтесь его улучшать. В конце концов, это должна быть база знаний ;-)
Эрик ван Бракель
Я думаю, что немного переборщил ... наслаждайтесь: D
Дэниел Шаффер
FWIW, пример Джеффа также требует, чтобы у вас была настраиваемая страница 404.
Дэниел Шаффер,
2
Одна из проблем с выбросом HttpException вместо простой установки HttpContext.Response.StatusCode = 404 заключается в том, что если вы используете обработчик OnException Controller (как я), он также перехватит HttpExceptions. Поэтому я думаю, что просто установка StatusCode - лучший подход.
Игорь Брейц
4
HttpException или HttpNotFoundResult в MVC3 полезны во многих отношениях. В случае @Igor Brejc, просто использовать , если заявление в OnException отфильтровывать не найдена ошибка.
CallMeLaNN 09
46

Делаем так; этот код находится вBaseController

/// <summary>
/// returns our standard page not found view
/// </summary>
protected ViewResult PageNotFound()
{
    Response.StatusCode = 404;
    return View("PageNotFound");
}

называется так

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
Джефф Этвуд
источник
подключено ли это действие к маршруту по умолчанию? Не вижу, как это будет выполняться.
Кристиан Далагер, 01
2
Его можно запустить так: защищенное переопределение void HandleUnknownAction (строка actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Тристан Уорнер-Смит,
Раньше я делал это таким образом, но обнаружил, что разделение результата и отображаемого представления было лучшим подходом. Посмотрите мой ответ ниже.
Брайан Валлелунга,
19
throw new HttpException(404, "Are you sure you're in the right place?");
Yfeldblum
источник
Мне это нравится, потому что он соответствует настраиваемым страницам ошибок, установленным в web.config.
Майк Коул
7

HttpNotFoundResult - отличный первый шаг к тому, что я использую. Возвращение HttpNotFoundResult - это хорошо. Тогда возникает вопрос, а что дальше?

Я создал фильтр действий под названием HandleNotFoundAttribute, который затем показывает страницу с ошибкой 404. Поскольку он возвращает представление, вы можете создать специальное представление 404 для каждого контроллера или позволить использовать общее представление 404 по умолчанию. Это будет вызвано даже тогда, когда в контроллере отсутствует указанное действие, поскольку фреймворк генерирует исключение HttpException с кодом состояния 404.

public class HandleNotFoundAttribute : ActionFilterAttribute, IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        var httpException = filterContext.Exception.GetBaseException() as HttpException;
        if (httpException != null && httpException.GetHttpCode() == (int)HttpStatusCode.NotFound)
        {
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; // Prevents IIS from intercepting the error and displaying its own content.
            filterContext.ExceptionHandled = true;
            filterContext.HttpContext.Response.StatusCode = (int) HttpStatusCode.NotFound;
            filterContext.Result = new ViewResult
                                        {
                                            ViewName = "404",
                                            ViewData = filterContext.Controller.ViewData,
                                            TempData = filterContext.Controller.TempData
                                        };
        }
    }
}
Брайан Валлелунга
источник
7

Обратите внимание, что с MVC3 вы можете просто использовать HttpStatusCodeResult.

Enashnash
источник
8
Или, что еще проще,HttpNotFoundResult
Мэтт Энрайт
6

Использование ActionFilter это трудно поддерживать , потому что всякий раз , когда мы бросаем об ошибке , необходимость фильтра устанавливается в атрибуте. Что, если мы забудем его установить? Один из способов - получение OnExceptionна базе контроллера. Вам необходимо определить BaseControllerпроизводный от, Controllerи все ваши контроллеры должны быть производными от BaseController. Лучше всего иметь базовый контроллер.

Обратите внимание, если используется Exceptionкод состояния ответа 500, поэтому нам нужно изменить его на 404 для Not Found и 401 для Unauthorized. Как я уже упоминал выше, используйте OnExceptionпереопределения, BaseControllerчтобы избежать использования атрибута фильтра.

Новый MVC 3 также усложняет задачу, возвращая браузеру пустое представление. Лучшее решение после некоторых исследований основано на моем ответе здесь. Как вернуть представление для HttpNotFound () в ASP.Net MVC 3?

Для большего удобства я вставляю его сюда:


После некоторого изучения. Обходной путь для MVC 3 здесь , чтобы получить все HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResultклассы и реализовывать новые (перекрывая его) HttpNotFound() метод в BaseController.

Лучше всего использовать базовый контроллер, чтобы у вас был «контроль» над всеми производными контроллерами.

Я создаю новый HttpStatusCodeResultкласс не для наследования, ActionResultа ViewResultдля рендеринга представления или любого другого, что Viewвы хотите, указав ViewNameсвойство. Я следую за оригинал , HttpStatusCodeResultчтобы установить HttpContext.Response.StatusCodeи HttpContext.Response.StatusDescriptionа затем base.ExecuteResult(context)сделает подходящий вид , потому что я снова проистекают из ViewResult. Достаточно просто? Надеюсь, это будет реализовано в ядре MVC.

Смотрите мой BaseControllerниже:

using System.Web;
using System.Web.Mvc;

namespace YourNamespace.Controllers
{
    public class BaseController : Controller
    {
        public BaseController()
        {
            ViewBag.MetaDescription = Settings.metaDescription;
            ViewBag.MetaKeywords = Settings.metaKeywords;
        }

        protected new HttpNotFoundResult HttpNotFound(string statusDescription = null)
        {
            return new HttpNotFoundResult(statusDescription);
        }

        protected HttpUnauthorizedResult HttpUnauthorized(string statusDescription = null)
        {
            return new HttpUnauthorizedResult(statusDescription);
        }

        protected class HttpNotFoundResult : HttpStatusCodeResult
        {
            public HttpNotFoundResult() : this(null) { }

            public HttpNotFoundResult(string statusDescription) : base(404, statusDescription) { }

        }

        protected class HttpUnauthorizedResult : HttpStatusCodeResult
        {
            public HttpUnauthorizedResult(string statusDescription) : base(401, statusDescription) { }
        }

        protected class HttpStatusCodeResult : ViewResult
        {
            public int StatusCode { get; private set; }
            public string StatusDescription { get; private set; }

            public HttpStatusCodeResult(int statusCode) : this(statusCode, null) { }

            public HttpStatusCodeResult(int statusCode, string statusDescription)
            {
                this.StatusCode = statusCode;
                this.StatusDescription = statusDescription;
            }

            public override void ExecuteResult(ControllerContext context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException("context");
                }

                context.HttpContext.Response.StatusCode = this.StatusCode;
                if (this.StatusDescription != null)
                {
                    context.HttpContext.Response.StatusDescription = this.StatusDescription;
                }
                // 1. Uncomment this to use the existing Error.ascx / Error.cshtml to view as an error or
                // 2. Uncomment this and change to any custom view and set the name here or simply
                // 3. (Recommended) Let it commented and the ViewName will be the current controller view action and on your view (or layout view even better) show the @ViewBag.Message to produce an inline message that tell the Not Found or Unauthorized
                //this.ViewName = "Error";
                this.ViewBag.Message = context.HttpContext.Response.StatusDescription;
                base.ExecuteResult(context);
            }
        }
    }
}

Чтобы использовать в своем действии вот так:

public ActionResult Index()
{
    // Some processing
    if (...)
        return HttpNotFound();
    // Other processing
}

И в _Layout.cshtml (например, на главной странице)

<div class="content">
    @if (ViewBag.Message != null)
    {
        <div class="inlineMsg"><p>@ViewBag.Message</p></div>
    }
    @RenderBody()
</div>

Кроме того, вы можете использовать настраиваемое представление, например, Error.shtmlили создать новое, NotFound.cshtmlкак я прокомментировал в коде, и вы можете определить модель представления для описания статуса и других объяснений.

CallMeLaNN
источник
Вы всегда можете зарегистрировать глобальный фильтр, который превосходит базовый контроллер, потому что вы должны ПОМНИТЬ использовать свой базовый контроллер!
Джон Калвинер,
:) Не уверен, что это все еще проблема в MVC4. В то время я имел в виду фильтр HandleNotFoundAttribute, на который ответил кто-то другой. Необязательно применять к каждому действию. Например, он подходит только для действия с параметром id, но не для действия Index (). Я согласился на глобальный фильтр, но не на HandleNotFoundAttribute, а на настраиваемый HandleErrorAttribute.
CallMeLaNN
Я думал, что у MVC3 тоже было, не уверен. Хорошее обсуждение независимо от того, кто может найти ответ
Джон Калвинер