¿Cuál es la forma correcta de enviar una respuesta HTTP 404 desde una acción ASP.NET MVC?

92

Si se le da la ruta:

{FeedName} / {ItemPermalink}

ej .: / Blog / Hello-World

Si el artículo no existe, quiero devolver un 404. ¿Cuál es la forma correcta de hacer esto en ASP.NET MVC?

Daniel Schaffer
fuente
Gracias por hacer esta pregunta por cierto. Esto va en mis adiciones de proyecto estándar: D
Erik van Brakel

Respuestas:

69

Disparando desde la cadera (codificación de vaquero ;-)), sugeriría algo como esto:

Controlador:

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

Con este enfoque, cumple con los estándares del marco. Ya hay un HttpUnauthorizedResult allí, por lo que esto simplemente ampliaría el marco a los ojos de otro desarrollador que mantendrá su código más adelante (ya sabe, el psicópata que sabe dónde vive).

Podría usar reflector para echar un vistazo al ensamblaje y ver cómo se logra HttpUnauthorizedResult, porque no sé si este enfoque pierde algo (casi parece demasiado simple).


Usé reflector para echar un vistazo al HttpUnauthorizedResult en este momento. Parece que están configurando el StatusCode en la respuesta a 0x191 (401). Aunque esto funciona para 401, usando 404 como el nuevo valor, parece que obtengo solo una página en blanco en Firefox. Sin embargo, Internet Explorer muestra un 404 predeterminado (no la versión ASP.NET). Usando la barra de herramientas del desarrollador web, inspeccioné los encabezados en FF, que SÍ muestran una respuesta 404 No encontrado. Podría ser simplemente algo que configuré mal en FF.


Dicho esto, creo que el enfoque de Jeff es un buen ejemplo de KISS. Si realmente no necesita la verbosidad en esta muestra, su método también funciona bien.

Erik van Brakel
fuente
Sí, también noté el Enum. Como dije, es solo un ejemplo burdo, siéntase libre de mejorarlo. Después de todo, se supone que es una base de conocimientos ;-)
Erik van Brakel
Creo que me excedí un poco ... disfruta: D
Daniel Schaffer
FWIW, el ejemplo de Jeff también requiere que tenga una página 404 personalizada.
Daniel Schaffer
2
Un problema con lanzar HttpException en lugar de simplemente configurar HttpContext.Response.StatusCode = 404 es que si usa el controlador OnException Controller (como yo lo hago), también detectará las HttpExceptions. Así que creo que simplemente configurar el StatusCode es un mejor enfoque.
Igor Brejc
4
HttpException o HttpNotFoundResult en MVC3 es útil de muchas maneras. En el caso de @Igor Brejc, simplemente use la instrucción if en OnException para filtrar el error no encontrado.
CallMeLaNN
46

Lo hacemos así; este código se encuentra enBaseController

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

llamado así

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
Jeff Atwood
fuente
¿Está esta acción luego conectada a una ruta predeterminada? No puedo ver cómo se ejecuta.
Christian Dalager
2
Podría ejecutarlo así: protected override void HandleUnknownAction (string actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Tristan Warner-Smith
Solía ​​hacerlo de esa manera, pero descubrí que dividir el resultado y la vista mostrada era un mejor enfoque. Mira mi respuesta a continuación.
Brian Vallelunga
19
throw new HttpException(404, "Are you sure you're in the right place?");
yfeldblum
fuente
Me gusta esto porque sigue las páginas de error personalizadas configuradas en web.config.
Mike Cole
7

HttpNotFoundResult es un gran primer paso para lo que estoy usando. Devolver un HttpNotFoundResult es bueno. Entonces la pregunta es, ¿qué sigue?

Creé un filtro de acción llamado HandleNotFoundAttribute que luego muestra una página de error 404. Dado que devuelve una vista, puede crear una vista 404 especial por controlador, o dejar que use una vista 404 compartida predeterminada. Esto incluso se llamará cuando un controlador no tenga la acción especificada presente, porque el marco arroja una HttpException con un código de estado 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
fuente
7

Tenga en cuenta que a partir de MVC3, solo puede usar HttpStatusCodeResult.

enashnash
fuente
8
O, más fácil,HttpNotFoundResult
Matt Enright
6

El uso de ActionFilter es difícil de mantener porque cada vez que arrojamos un error, el filtro debe establecerse en el atributo. ¿Qué pasa si nos olvidamos de configurarlo? Una forma es derivar OnExceptionen el controlador base. Debe definir un BaseControllerderivado de Controllery todos los controladores deben derivar de BaseController. Es una buena práctica tener un controlador base.

Tenga en cuenta que si Exceptionel código de estado de respuesta es 500, debemos cambiarlo a 404 para No encontrado y 401 para No autorizado. Como mencioné anteriormente, use OnExceptionanulaciones BaseControllerpara evitar usar el atributo de filtro.

El nuevo MVC 3 también hace más problemático al devolver una vista vacía al navegador. La mejor solución después de algunas investigaciones se basa en mi respuesta aquí. ¿Cómo devolver una vista para HttpNotFound () en ASP.Net MVC 3?

Para hacer más conveniencia lo pego aquí:


Después de un poco de estudio. La solución para MVC 3 aquí es derivar todos HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResultclases e implementar nueva (anulando ella) HttpNotFoundmétodo () en BaseController.

Es una buena práctica utilizar el controlador base para que tenga "control" sobre todos los controladores derivados.

Creo una nueva HttpStatusCodeResultclase, no para derivar ActionResultsino desde ViewResultpara representar la vista o cualquiera Viewque desee especificando la ViewNamepropiedad. Sigo el original HttpStatusCodeResultpara establecer el HttpContext.Response.StatusCodey, HttpContext.Response.StatusDescriptionpero luego base.ExecuteResult(context)renderizaré la vista adecuada porque de nuevo derivaré de ViewResult. ¿Es bastante simple? Espero que esto se implemente en el núcleo MVC.

Mira mi BaseControllerbramido:

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

Para usar en su acción de esta manera:

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

Y en _Layout.cshtml (como página maestra)

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

Además, puede usar una vista personalizada como Error.shtmlo crear una nueva NotFound.cshtmlcomo comenté en el código y puede definir un modelo de vista para la descripción del estado y otras explicaciones.

CallMeLaNN
fuente
¡Siempre puedes registrar un filtro global que supere a un controlador base porque tienes que RECORDAR usar tu controlador base!
John Culviner
:) No estoy seguro de que esto siga siendo un problema en MVC4. Lo que quiero decir en ese momento es el filtro HandleNotFoundAttribute respondido por otra persona. No es necesario aplicarlo para cada acción. Por ejemplo, solo es adecuado para la acción que tiene id param pero no la acción Index (). Acepté un filtro global, no para HandleNotFoundAttribute sino para un HandleErrorAttribute personalizado.
CallMeLaNN
Pensé que MVC3 también lo tenía, no estoy seguro. Buena discusión independientemente para otros que puedan encontrar la respuesta
John Culviner