Jaki jest właściwy sposób wysyłania odpowiedzi HTTP 404 z akcji ASP.NET MVC?

92

Jeśli podano trasę:

{FeedName} / {ItemPermalink}

np .: / Blog / Hello-World

Jeśli element nie istnieje, chcę zwrócić 404. Jaki jest właściwy sposób zrobienia tego w ASP.NET MVC?

Daniel Schaffer
źródło
Przy okazji dzięki za zadanie tego pytania. To dzieje się w moich standardowych dodatkach do projektu: D
Erik van Brakel

Odpowiedzi:

69

Strzelając z biodra (kodowanie kowbojskie ;-)), proponuję coś takiego:

Kontroler:

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

Stosując takie podejście, przestrzegasz standardów ramowych. Jest tam już HttpUnauthorizedResult, więc to po prostu rozszerzyłoby strukturę w oczach innego dewelopera, który później utrzymuje twój kod (wiesz, psychol, który wie, gdzie mieszkasz).

Możesz użyć reflektora, aby przyjrzeć się asemblacji, aby zobaczyć, jak osiągnięto HttpUnauthorizedResult, ponieważ nie wiem, czy to podejście coś pomija (wydaje się prawie zbyt proste).


Właśnie użyłem reflektora, aby przyjrzeć się HttpUnauthorizedResult. Wygląda na to, że ustawiają StatusCode w odpowiedzi na 0x191 (401). Chociaż działa to dla 401, używając 404 jako nowej wartości, wydaje się, że w Firefoksie pojawia się tylko pusta strona. Internet Explorer pokazuje jednak domyślny 404 (nie wersję ASP.NET). Korzystając z paska narzędzi webdeveloper, przejrzałem nagłówki w FF, które DO pokazują odpowiedź 404 Not Found. Może to być po prostu coś, co źle skonfigurowałem w FF.


Biorąc to pod uwagę, myślę, że podejście Jeffa jest dobrym przykładem KISS. Jeśli tak naprawdę nie potrzebujesz szczegółowości w tym przykładzie, jego metoda również działa dobrze.

Erik van Brakel
źródło
Tak, zauważyłem również Enum. Jak powiedziałem, to tylko prymitywny przykład, nie krępuj się go ulepszyć. W końcu to ma być baza wiedzy ;-)
Erik van Brakel
Myślę, że trochę przesadziłem ... ciesz się: D
Daniel Schaffer
FWIW, przykład Jeffa również wymaga posiadania niestandardowej strony 404.
Daniel Schaffer
2
Jednym z problemów z wyrzucaniem HttpException zamiast po prostu ustawiania HttpContext.Response.StatusCode = 404 jest to, że jeśli używasz programu obsługi kontrolera OnException (tak jak ja), to również przechwytuje HttpExceptions. Więc myślę, że samo ustawienie StatusCode jest lepszym podejściem.
Igor Brejc
4
HttpException lub HttpNotFoundResult w MVC3 jest przydatny na wiele sposobów. W przypadku @Igor Brejc, po prostu użyj instrukcji if w OnException, aby odfiltrować nie znaleziony błąd.
CallMeLaNN
46

Robimy to w ten sposób; ten kod znajduje się wBaseController

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

tak nazywany

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
Jeff Atwood
źródło
czy ta akcja jest następnie połączona z domyślną trasą? Nie widzę, jak to się robi.
Christian Dalager
2
Mógłby to wyglądać tak: protected override void HandleUnknownAction (string actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Tristan Warner-Smith
Kiedyś robiłem to w ten sposób, ale stwierdziłem, że podzielenie wyniku i wyświetlanego widoku było lepszym podejściem. Sprawdź moją odpowiedź poniżej.
Brian Vallelunga
19
throw new HttpException(404, "Are you sure you're in the right place?");
yfeldblum
źródło
Podoba mi się to, ponieważ jest zgodne z niestandardowymi stronami błędów skonfigurowanymi w web.config.
Mike Cole
7

HttpNotFoundResult to świetny pierwszy krok do tego, czego używam. Zwracanie HttpNotFoundResult jest dobre. W takim razie pytanie brzmi: co dalej?

Utworzyłem filtr akcji o nazwie HandleNotFoundAttribute, który następnie wyświetla stronę błędu 404. Ponieważ zwraca widok, możesz utworzyć specjalny widok 404 na kontroler lub użyć domyślnego udostępnionego widoku 404. Będzie to nawet wywoływane, gdy kontroler nie ma określonej akcji, ponieważ struktura zgłasza HttpException z kodem stanu 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
                                        };
        }
    }
}
Brian Vallelunga
źródło
7

Zwróć uwagę, że od MVC3 możesz po prostu użyć HttpStatusCodeResult.

enashnash
źródło
8
Albo jeszcze łatwiejHttpNotFoundResult
Matt Enright,
6

Korzystanie z ActionFilter jest trudne do utrzymania, ponieważ za każdym razem, gdy zgłaszamy błąd, filtr musi być ustawiony w atrybucie. A co, jeśli zapomnimy go ustawić? Jednym ze sposobów jest wyprowadzenie OnExceptionna podstawie kontrolera. Musisz zdefiniować element BaseControllerpochodny Controlleri wszystkie kontrolery muszą pochodzić z BaseController. Najlepszą praktyką jest posiadanie podstawowego kontrolera.

Zwróć uwagę, jeśli używasz Exceptionkodu stanu odpowiedzi 500, więc musimy zmienić go na 404 dla Nie znaleziono i 401 dla Nieautoryzowany. Tak jak wspomniałem powyżej, użyj OnExceptionzastąpień, BaseControlleraby uniknąć używania atrybutu filtra.

Nowy MVC 3 jest również bardziej kłopotliwy, zwracając pusty widok do przeglądarki. Najlepsze rozwiązanie po niektórych badaniach opiera się na mojej odpowiedzi tutaj. Jak zwrócić widok dla HttpNotFound () w ASP.Net MVC 3?

Dla większej wygody wklejam go tutaj:


Po kilku badaniach. Rozwiązaniem dla MVC 3 jest tu czerpać wszystkie HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResultzajęcia i wdrożyć nową (przesłanianie go) HttpNotFound(metoda) w BaseController.

Najlepszą praktyką jest używanie kontrolera podstawowego, aby mieć „kontrolę” nad wszystkimi kontrolerami pochodnymi.

Tworzę nową HttpStatusCodeResultklasę, nie po to, aby pochodzić z, ActionResultale z, ViewResultaby renderować widok lub dowolną inną View, określając ViewNamewłaściwość. Podążam za oryginałem, HttpStatusCodeResultaby ustawić, HttpContext.Response.StatusCodea HttpContext.Response.StatusDescriptionnastępnie base.ExecuteResult(context)wyrenderuję odpowiedni widok, ponieważ ponownie wywodzę się z ViewResult. Wystarczająco proste, prawda? Mam nadzieję, że zostanie to zaimplementowane w rdzeniu MVC.

Zobacz mój BaseControllerponiżej:

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

Aby użyć w swoim działaniu w następujący sposób:

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

I w _Layout.cshtml (jak strona wzorcowa)

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

Dodatkowo możesz użyć niestandardowego widoku, takiego jak Error.shtmllub utworzyć nowy, NotFound.cshtmljak skomentowałem w kodzie, a także możesz zdefiniować model widoku dla opisu stanu i innych wyjaśnień.

CallMeLaNN
źródło
Zawsze możesz zarejestrować globalny filtr, który bije kontroler podstawowy, ponieważ musisz PAMIĘTAĆ, aby używać kontrolera podstawowego!
John Culviner
:) Nie jestem pewien, czy to nadal jest problem w MVC4. W tym czasie mam na myśli filtr HandleNotFoundAttribute, na który odpowiada ktoś inny. Nie jest konieczne stosowanie go do każdej czynności. Np. Nadaje się tylko do akcji, które mają parametr id, ale nie do akcji Index (). Zgodziłem się na globalny filtr, nie dla HandleNotFoundAttribute, ale niestandardowego HandleErrorAttribute.
CallMeLaNN
Myślałem, że MVC3 też to ma, nie jestem pewien. Dobra dyskusja niezależnie od innych, którzy mogą spotkać się z odpowiedzią
John Culviner