Qual è il modo corretto per inviare una risposta HTTP 404 da un'azione ASP.NET MVC?


92

Se indicato il percorso:

{FeedName} / {ItemPermalink}

es: / Blog / Hello-World

Se l'elemento non esiste, voglio restituire un 404. Qual è il modo giusto per farlo in ASP.NET MVC?


Grazie per aver posto questa domanda tra l'altro. Questo è nelle mie aggiunte al progetto standard: D
Erik van Brakel

Risposte:


69

Scattando dall'anca (codifica cowboy ;-)), suggerirei qualcosa del genere:

Controller:

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

Usando questo approccio sei conforme agli standard del framework. C'è già un HttpUnauthorizedResult lì dentro, quindi questo estenderebbe semplicemente il framework agli occhi di un altro sviluppatore che manterrà il tuo codice in seguito (sai, lo psicopatico che sa dove vivi).

Potresti usare reflector per dare un'occhiata all'assembly per vedere come si ottiene HttpUnauthorizedResult, perché non so se questo approccio manchi qualcosa (sembra quasi troppo semplice).


Ho usato riflettore per dare un'occhiata a HttpUnauthorizedResult proprio ora. Sembra che stiano impostando lo StatusCode sulla risposta a 0x191 (401). Sebbene questo funzioni per 401, utilizzando 404 come nuovo valore mi sembra di ottenere solo una pagina vuota in Firefox. Internet Explorer mostra un 404 predefinito (non la versione ASP.NET). Utilizzando la barra degli strumenti del webdeveloper ho ispezionato le intestazioni in FF, che mostrano una risposta 404 Not Found. Potrebbe essere semplicemente qualcosa che ho configurato male in FF.


Detto questo, penso che l'approccio di Jeff sia un ottimo esempio di KISS. Se non hai davvero bisogno della verbosità in questo esempio, anche il suo metodo funziona bene.


Sì, ho notato anche l'Enum. Come ho detto, è solo un semplice esempio, sentiti libero di migliorarlo. Dopotutto, questa dovrebbe essere una knowledgebase ;-)
Erik van Brakel

Penso di essere andato un po 'fuori bordo ... divertiti: D
Daniel Schaffer

FWIW, l'esempio di Jeff richiede anche che tu abbia una pagina 404 personalizzata.
Daniel Schaffer

2
Un problema con il lancio di HttpException invece di impostare solo HttpContext.Response.StatusCode = 404 è che se usi il gestore OnException Controller (come faccio io), catturerà anche HttpExceptions. Quindi penso che impostare semplicemente lo StatusCode sia un approccio migliore.
Igor Brejc

4
HttpException o HttpNotFoundResult in MVC3 è utile in molti modi. In caso di @Igor Brejc, usa semplicemente l' istruzione if nell'OnException per filtrare l'errore non trovato.
CallMeLaNN

46

Lo facciamo così; questo codice si trova inBaseController

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

chiamato così

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();

questa azione è quindi collegata a un percorso predefinito? Non riesco a vedere come viene eseguito.
Christian Dalager

2
Potrebbe essere in esecuzione in questo modo: protected override void HandleUnknownAction (stringa actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Tristan Warner-Smith

Lo facevo in questo modo, ma ho scoperto che dividere il risultato e la vista visualizzata era un approccio migliore. Controlla la mia risposta qui sotto.
Brian Vallelunga

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

Mi piace perché segue le pagine di errore personalizzate impostate in web.config.
Mike Cole

7

HttpNotFoundResult è un ottimo primo passo per quello che sto usando. La restituzione di un HttpNotFoundResult è buona. Allora la domanda è: qual è il prossimo?

Ho creato un filtro di azione chiamato HandleNotFoundAttribute che poi mostra una pagina di errore 404. Poiché restituisce una visualizzazione, è possibile creare una visualizzazione 404 speciale per controller o utilizzare una visualizzazione 404 condivisa predefinita. Questo verrà anche chiamato quando un controller non ha l'azione specificata presente, perché il framework genera una HttpException con un codice di stato 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

Nota che a partire da MVC3, puoi semplicemente usare HttpStatusCodeResult.


8
O, ancora più semplice,HttpNotFoundResult
Matt Enright

6

L'uso di ActionFilter è difficile da mantenere perché ogni volta che viene generato un errore il filtro deve essere impostato nell'attributo. E se ci dimentichiamo di impostarlo? Un modo è derivare OnExceptiondal controller di base. È necessario definire un BaseControllerderivato da Controllere tutti i controller devono derivare BaseController. È consigliabile disporre di un controller di base.

Nota se l'utilizzo Exceptiondel codice di stato della risposta è 500, quindi è necessario modificarlo in 404 per Non trovato e 401 per Non autorizzato. Proprio come ho detto sopra, usa le OnExceptionsostituzioni BaseControllerper evitare di usare l'attributo di filtro.

Il nuovo MVC 3 rende anche più problematico restituendo una vista vuota al browser. La soluzione migliore dopo alcune ricerche si basa sulla mia risposta qui. Come restituire una vista per HttpNotFound () in ASP.Net MVC 3?

Per rendere più comoda la incollo qui:


Dopo un po 'di studio. La soluzione per MVC 3 è quello di trarre tutti HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResultle classi e implementare nuove (l'override di esso) HttpNotFoundil metodo () in BaseController.

È buona norma utilizzare il controller di base in modo da avere il "controllo" su tutti i controller derivati.

Creo una nuova HttpStatusCodeResultclasse, non da cui derivare ActionResultma da cui ViewResultrenderizzare la vista o qualsiasi altra Viewtu voglia specificando la ViewNameproprietà. Seguo l'originale HttpStatusCodeResultper impostare HttpContext.Response.StatusCodee HttpContext.Response.StatusDescriptionma poi base.ExecuteResult(context)renderò la vista adatta perché di nuovo derivo da ViewResult. Abbastanza semplice è? Spero che questo venga implementato nel core MVC.

Vedi il mio BaseControllermuggito:

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

Da utilizzare nella tua azione in questo modo:

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

E in _Layout.cshtml (come la pagina principale)

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

Inoltre puoi usare una vista personalizzata come Error.shtmlo crearne di nuove NotFound.cshtmlcome ho commentato nel codice e puoi definire un modello di vista per la descrizione dello stato e altre spiegazioni.


Puoi sempre registrare un filtro globale che batte un controller di base perché devi RICORDARE di utilizzare il controller di base!
John Culviner

:) Non sono sicuro che questo sia ancora un problema in MVC4. Quello che intendo in quel momento è che il filtro HandleNotFoundAttribute ha risposto da qualcun altro. Non è necessario essere applicato per ogni azione. Ad esempio, è adatto solo per azioni che hanno id param ma non Index (). Ho accettato il filtro globale, non per HandleNotFoundAttribute ma per HandleErrorAttribute personalizzato.
CallMeLaNN

Pensavo che anche MVC3 lo avesse, non ne sono sicuro. Buona discussione a prescindere da altri che potrebbero trovare la risposta
John Culviner
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.