Який правильний спосіб надіслати відповідь 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 Не знайдено. Це може бути просто те, що я неправильно налаштував у FF.


Враховуючи це, я думаю, що підхід Джеффа є прекрасним прикладом KISS. Якщо вам насправді не потрібна багатослівність у цьому зразку, його метод також чудово працює.


Так, я також помітив Enum. Як я вже сказав, це лише грубий приклад, сміливо вдосконалюйте його. Це, мабуть, все-таки база знань ;-)
Ерік ван Бракель

Думаю, я трохи переборщив ... насолоджуйтесь: D
Даніель Шаффер

FWIW, приклад Джефа також вимагає, щоб у вас була спеціальна сторінка 404.
Даніель Шаффер

2
Однією з проблем з киданням HttpException замість того, щоб просто встановити HttpContext.Response.StatusCode = 404 є те, що якщо ви використовуєте обробник OnException Controller (як я це роблю), він також схопить HttpExceptions. Тому я думаю, що просто встановлення StatusCode є кращим підходом.
Ігор Брейц,

4
HttpException або HttpNotFoundResult у MVC3 багато в чому корисні. У випадку з @Igor Brejc, просто використовуйте оператор if у OnException, щоб відфільтрувати помилку, яку не знайшли.
CallMeLaNN

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

чи ця дія потім підключена до маршруту за замовчуванням? Не бачу, як це може бути виконано.
Крістіан Далагер

2
Може виконуватися так: захищений перевизначення void HandleUnknownAction (рядок actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Трістан Уорнер-Сміт

Раніше я робив це так, але виявив, що розділення результату та відображеного подання було кращим підходом. Перевірте мою відповідь нижче.
Брайан Валлелунга

19
throw new HttpException(404, "Are you sure you're in the right place?");

Мені це подобається, оскільки воно відповідає спеціальним сторінкам помилок, встановленим у web.config.
Mike Cole

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


6

Використання ActionFilter це важко підтримувати , тому що кожного разу , коли ми кидаємо про помилку , необхідність фільтра встановлюється в атрибуті. Що якщо ми забудемо встановити його? Одним із способів є отримання OnExceptionна базовому контролері. Вам потрібно визначити BaseControllerпохідне від, Controllerі всі ваші контролери повинні походити зBaseController . Найкращою практикою є наявність базового контролера.

Зверніть увагу, якщо використовується Exceptionкод стану відповіді 500, тому нам потрібно змінити його на 404 для Не знайдено та 401 для Несанкціонованого. Як я вже згадував вище, використовуйте OnExceptionперевизначення, BaseControllerщоб уникнути використання атрибута filter.

Новий 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як я коментував у коді, і ви можете визначити модель перегляду для опису стану та інших пояснень.


Ви завжди можете зареєструвати глобальний фільтр, який перевершує базовий контролер, тому що ви повинні ПАМ’ЯТИ, щоб використовувати ваш базовий контролер!
Джон Калвінер

:) Не впевнений, що це все ще проблема в MVC4. Тоді я маю на увазі фільтр HandleNotFoundAttribute, на який відповідає хтось інший. Не потрібно застосовувати для кожної дії. Наприклад, він підходить лише для дій, які мають параметр id, але не дію Index (). Я погодився на глобальний фільтр, не для HandleNotFoundAttribute, а спеціальний HandleErrorAttribute.
CallMeLaNN

Я думав, у MVC3 він теж був, не впевнений. Хороша дискусія незалежно від інших, хто може натрапити на відповідь
Джон Калвінер,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.