web-dev-qa-db-fra.com

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

Si l'itinéraire est donné:

{FeedName}/{ItemPermalink}

ex:/Blog/Hello-World

Si l'article n'existe pas, je veux retourner un 404. Quelle est la bonne façon de le faire dans ASP.NET MVC?

90
Daniel Schaffer

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

Contrôleur:

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-cadres. Il y a déjà un HttpUnauthorizedResult, donc cela étendrait simplement le cadre aux yeux d'un autre développeur maintenant votre code plus tard (vous savez, le psycho qui sait où vous vivez).

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


J'ai utilisé le réflecteur pour jeter un œil au HttpUnauthorizedResult tout à l'heure. 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 j'obtienne juste 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. Cela pourrait ê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.

68
Erik van Brakel

Nous le faisons comme ça; ce code se trouve dans BaseController

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

appelé ainsi

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
46
Jeff Atwood
throw new HttpException(404, "Are you sure you're in the right place?");
19
yfeldblum

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?

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 bien 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
                                        };
        }
    }
}
7
Brian Vallelunga

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

7
enashnash

L'utilisation de ActionFilter est difficile à maintenir car chaque fois que nous lançons une erreur, le filtre doivent être définis dans l'attribut. Et si nous oublions de le régler? Une façon consiste à dériver OnException sur le contrôleur de base. Vous devez définir un BaseController dérivé de Controller et tous vos contrôleurs doivent dériver de BaseController. Il est préférable d'avoir un contrôleur de base.

Notez que si vous utilisez Exception le code d'état de la réponse est 500, nous devons donc le changer en 404 pour Not Found et 401 pour Unauthorized. Tout comme je l'ai mentionné ci-dessus, utilisez OnException substitue sur BaseController pour éviter d'utiliser l'attribut filter.

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 retourner 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 consiste à dériver toutes les classes HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResult et implémenter la nouvelle méthode (la remplacer) HttpNotFound () dans BaseController.

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

Je crée une nouvelle classe HttpStatusCodeResult, non pour dériver de ActionResult mais de ViewResult pour rendre la vue ou tout View que vous voulez en spécifiant le ViewName propriété. Je suis le HttpStatusCodeResult d'origine pour définir le HttpContext.Response.StatusCode Et HttpContext.Response.StatusDescription Mais ensuite base.ExecuteResult(context) rendra la vue appropriée car encore une fois je dérive de ViewResult. C'est assez simple? J'espère que cela sera implémenté dans le noyau MVC.

Voir mon BaseController ci-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 principale)

<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.shtml Ou créer de nouveaux NotFound.cshtml Comme je l'ai commenté dans le code et vous pouvez définir un modèle de vue pour la description de l'état et d'autres explications.

5
CallMeLaNN