Quelle est la bonne façon d'envoyer une réponse HTTP 404 à partir d'une action ASP.NET MVC?

92

Si l'itinéraire est donné:

{FeedName} / {ItemPermalink}

ex: / Blog / Hello-World

Si l'élément n'existe pas, je souhaite renvoyer un 404. Quelle est la bonne façon de procéder dans ASP.NET MVC?

Daniel Schaffer
la source
Merci d'avoir posé cette question btw. Cela se passe dans mes ajouts de projet standard: D
Erik van Brakel

Réponses:

69

Tirant de la hanche (codage cowboy ;-)), je suggère quelque chose comme ceci:

Manette:

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

En utilisant cette approche, vous vous conformez aux normes du cadre. Il y a déjà un HttpUnauthorizedResult là-dedans, donc cela élargirait simplement le cadre aux yeux d'un autre développeur qui maintiendrait votre code plus tard (vous savez, le psychopathe qui sait où vous habitez).

Vous pouvez utiliser réflecteur pour jeter un œil dans l'assemblage pour voir comment le HttpUnauthorizedResult est atteint, car je ne sais pas si cette approche manque quelque chose (cela semble presque trop simple).


J'ai utilisé le réflecteur pour jeter un coup d'œil à HttpUnauthorizedResult tout à l'heure. Il semble qu'ils définissent le StatusCode sur la réponse à 0x191 (401). Bien que cela fonctionne pour 401, en utilisant 404 comme nouvelle valeur, il semble que je n'obtienne qu'une page vierge dans Firefox. Internet Explorer affiche cependant un 404 par défaut (pas la version ASP.NET). En utilisant la barre d'outils du développeur Web, j'ai inspecté les en-têtes dans FF, qui affichent une réponse 404 Not Found. Peut-être simplement quelque chose que j'ai mal configuré dans FF.


Cela étant dit, je pense que l'approche de Jeff est un bel exemple de KISS. Si vous n'avez pas vraiment besoin de la verbosité de cet exemple, sa méthode fonctionne également très bien.

Erik van Brakel
la source
Ouais, j'ai aussi remarqué l'Enum. Comme je l'ai dit, ce n'est qu'un exemple grossier, n'hésitez pas à l'améliorer. C'est censé être une base de connaissances après tout ;-)
Erik van Brakel
Je pense que je suis allé un peu trop loin ... profiter: D
Daniel Schaffer
FWIW, l'exemple de Jeff nécessite également que vous disposiez d'une page 404 personnalisée.
Daniel Schaffer
2
Un problème avec le lancement de HttpException au lieu de simplement définir le HttpContext.Response.StatusCode = 404 est que si vous utilisez le gestionnaire OnException Controller (comme je le fais), il attrapera également les HttpExceptions. Je pense donc que le simple réglage du StatusCode est une meilleure approche.
Igor Brejc
4
HttpException ou HttpNotFoundResult dans MVC3 est utile à bien des égards. Dans le cas de @Igor Brejc, utilisez simplement l' instruction if dans OnException pour filtrer l'erreur non trouvée.
CallMeLaNN
46

Nous le faisons comme ça; ce code se trouve dansBaseController

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

appelé comme ça

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
Jeff Atwood
la source
cette action est-elle ensuite câblée à une route par défaut? Je ne vois pas comment il peut être exécuté.
Christian Dalager
2
Peut-être l'exécuter comme ceci: protected override void HandleUnknownAction (string actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Tristan Warner-Smith
J'avais l'habitude de le faire de cette façon, mais j'ai trouvé que diviser le résultat et la vue affichée était une meilleure approche. Découvrez ma réponse ci-dessous.
Brian Vallelunga
19
throw new HttpException(404, "Are you sure you're in the right place?");
Yfeldblum
la source
J'aime cela car il suit les pages d'erreur personnalisées configurées dans web.config.
Mike Cole
7

Le HttpNotFoundResult est une excellente première étape vers ce que j'utilise. Renvoyer un HttpNotFoundResult est bon. Alors la question est, quelle est la prochaine étape?

J'ai créé un filtre d'action appelé HandleNotFoundAttribute qui affiche ensuite une page d'erreur 404. Puisqu'il renvoie une vue, vous pouvez créer une vue 404 spéciale par contrôleur, ou laisser est utiliser une vue 404 partagée par défaut. Cela sera même appelé lorsqu'un contrôleur n'a pas l'action spécifiée présente, car le framework lève une HttpException avec un code d'état de 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
la source
7

Notez qu'à partir de MVC3, vous pouvez simplement utiliser HttpStatusCodeResult.

enashnash
la source
8
Ou, encore plus facile,HttpNotFoundResult
Matt Enright
6

L'utilisation d' ActionFilter est difficile à gérer car chaque fois que nous lançons une erreur, le filtre doit être défini dans l'attribut. Et si on oublie de le régler? Une façon consiste à dériver OnExceptionsur le contrôleur de base. Vous devez définir un BaseControllerdérivé de Controlleret tous vos contrôleurs doivent dériver deBaseController . Il est recommandé d'avoir un contrôleur de base.

Notez que si Exceptionle code d'état de réponse est 500, nous devons le changer en 404 pour Non trouvé et 401 pour Non autorisé. Comme je l'ai mentionné ci-dessus, utilisez les OnExceptionremplacements surBaseController pour éviter d'utiliser l'attribut de filtre.

Le nouveau MVC 3 rend également plus gênant en renvoyant une vue vide au navigateur. La meilleure solution après quelques recherches est basée sur ma réponse ici Comment renvoyer une vue pour HttpNotFound () dans ASP.Net MVC 3?

Pour plus de commodité, je le colle ici:


Après quelques études. La solution de contournement pour MVC 3 ici est de tirer tous HttpNotFoundResult, HttpUnauthorizedResult, les HttpStatusCodeResultclasses et mettre en œuvre une nouvelle ( la redéfinir) HttpNotFoundméthode () dansBaseController .

Il est recommandé d'utiliser le contrôleur de base pour avoir un «contrôle» sur tous les contrôleurs dérivés.

Je crée une nouvelle HttpStatusCodeResultclasse, non pas pour dériver ActionResultmais ViewResultpour rendre la vue ou celle que Viewvous voulez en spécifiant la ViewNamepropriété. Je suis l'original HttpStatusCodeResultpour définir le HttpContext.Response.StatusCodeet HttpContext.Response.StatusDescriptionpuis base.ExecuteResult(context)je rendrai la vue appropriée car encore une fois je dérive ViewResult. Est-ce assez simple? J'espère que cela sera implémenté dans le noyau MVC.

Voir mon BaseControllerci - dessous:

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

À utiliser dans votre action comme ceci:

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

Et dans _Layout.cshtml (comme la page maître)

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

De plus, vous pouvez utiliser une vue personnalisée comme Error.shtmlou créer une nouvelle NotFound.cshtmlcomme j'ai commenté dans le code et vous pouvez définir un modèle de vue pour la description du statut et d'autres explications.

CallMeLaNN
la source
Vous pouvez toujours enregistrer un filtre global qui bat un contrôleur de base car vous devez VOUS RAPPELER d'utiliser votre contrôleur de base!
John Culviner
:) Pas sûr non plus que ce soit toujours un problème dans MVC4. Ce que je veux dire à ce moment-là, c'est le filtre HandleNotFoundAttribute auquel quelqu'un d'autre répond. Il n'est pas nécessaire d'être appliqué pour chaque action. Par exemple, il ne convient que pour l'action qui a un paramètre id mais pas l'action Index (). J'ai accepté le filtre global, pas pour HandleNotFoundAttribute mais un HandleErrorAttribute personnalisé.
CallMeLaNN
Je pensais que MVC3 l'avait aussi, pas sûr. Bonne discussion, peu importe pour les autres qui peuvent trouver la réponse
John Culviner