Qual é a maneira correta de enviar uma resposta HTTP 404 de uma ação ASP.NET MVC?

92

Se for dada a rota:

{FeedName} / {ItemPermalink}

ex: / Blog / Hello-World

Se o item não existir, desejo retornar um 404. Qual é a maneira certa de fazer isso na ASP.NET MVC?

Daniel Schaffer
fonte
Obrigado por fazer esta pergunta btw. Isso está acontecendo em minhas adições de projeto padrão: D
Erik van Brakel

Respostas:

69

Tirando fotos do quadril (codificação de cowboy ;-)), sugiro algo assim:

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

Usando essa abordagem, você cumpre os padrões da estrutura. Já existe um HttpUnauthorizedResult lá, então isso simplesmente estenderia a estrutura aos olhos de outro desenvolvedor que manterá seu código posteriormente (você sabe, o psicopata que sabe onde você mora).

Você poderia usar o refletor para dar uma olhada na montagem para ver como o HttpUnauthorizedResult é alcançado, porque eu não sei se essa abordagem deixa escapar alguma coisa (parece quase muito simples).


Eu usei o refletor para dar uma olhada no HttpUnauthorizedResult agora mesmo. Parece que eles estão definindo o StatusCode na resposta para 0x191 (401). Embora isso funcione para 401, usando 404 como o novo valor, parece que estou recebendo apenas uma página em branco no Firefox. O Internet Explorer mostra um 404 padrão (não a versão ASP.NET). Usando a barra de ferramentas do webdeveloper, inspecionei os cabeçalhos no FF, que mostram uma resposta 404 Not Found. Pode ser simplesmente algo que configurei incorretamente no FF.


Dito isso, acho que a abordagem de Jeff é um bom exemplo de KISS. Se você realmente não precisa da verbosidade neste exemplo, seu método também funciona bem.

Erik van Brakel
fonte
Sim, também notei o Enum. Como eu disse, é apenas um exemplo bruto, fique à vontade para aprimorá-lo. Afinal de contas, esta deveria ser uma base de conhecimento ;-)
Erik van Brakel
Acho que exagerei ... divirta-se: D
Daniel Schaffer
FWIW, o exemplo de Jeff também requer que você tenha uma página 404 personalizada.
Daniel Schaffer
2
Um problema em lançar HttpException em vez de apenas definir HttpContext.Response.StatusCode = 404 é se você usar o manipulador OnException Controller (como eu), ele também capturará HttpExceptions. Acho que apenas definir o StatusCode é uma abordagem melhor.
Igor Brejc
4
HttpException ou HttpNotFoundResult em MVC3 é útil de várias maneiras. No caso de @Igor Brejc, basta usar a instrução if na OnException para filtrar o erro não encontrado.
CallMeLaNN,
46

Nós fazemos assim; este código é encontrado emBaseController

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

chamado assim

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
Jeff Atwood
fonte
esta ação está conectada a uma rota padrão? Não consigo ver como é executado.
Christian Dalager
2
Pode ser executado assim: protected override void HandleUnknownAction (string actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Tristan Warner-Smith
Eu costumava fazer isso dessa forma, mas descobri que dividir o resultado e a exibição exibida era uma abordagem melhor. Confira minha resposta abaixo.
Brian Vallelunga
19
throw new HttpException(404, "Are you sure you're in the right place?");
Yfeldblum
fonte
Gosto disso porque segue as páginas de erro personalizadas configuradas em web.config.
Mike Cole
7

O HttpNotFoundResult é uma ótima primeira etapa para o que estou usando. Retornar um HttpNotFoundResult é bom. Então a questão é: o que vem a seguir?

Eu criei um filtro de ação chamado HandleNotFoundAttribute que mostra uma página de erro 404. Uma vez que retorna uma visão, você pode criar uma visão 404 especial por controlador, ou vamos usar uma visão 404 compartilhada padrão. Ele ainda será chamado quando um controlador não tiver a ação especificada presente, porque a estrutura lança uma HttpException com um código de status 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
fonte
7

Observe que a partir de MVC3, você pode apenas usar HttpStatusCodeResult.

enashnash
fonte
8
Ou, ainda mais fácil,HttpNotFoundResult
Matt Enright
6

Usar ActionFilter é difícil de manter porque sempre que lançamos um erro, o filtro precisa ser definido no atributo. E se esquecermos de configurá-lo? Uma maneira é derivar OnExceptionno controlador de base. Você precisa definir um BaseControllerderivado de Controllere todos os seus controladores devem derivar BaseController. É uma prática recomendada ter um controlador de base.

Observe que se Exceptiono código de status de resposta for 500, precisamos alterá-lo para 404 para Não encontrado e 401 para Não autorizado. Assim como mencionei acima, use OnExceptionsubstituições BaseControllerpara evitar o uso do atributo de filtro.

O novo MVC 3 também torna mais problemático, retornando uma visão vazia ao navegador. A melhor solução depois de alguma pesquisa é baseada na minha resposta aqui. Como retornar uma visualização para HttpNotFound () no ASP.Net MVC 3?

Para tornar mais conveniente, colo aqui:


Depois de algum estudo. A solução alternativa para MVC 3 aqui é para derivar todas HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResultaulas e implementar novo (substituindo-lo) HttpNotFoundmétodo () in BaseController.

É uma prática recomendada usar o controlador básico para que você tenha 'controle' sobre todos os controladores derivados.

Eu crio uma nova HttpStatusCodeResultclasse, não para derivar, ActionResultmas ViewResultpara renderizar a vista ou qualquer outra que Viewvocê queira especificando a ViewNamepropriedade. Sigo o original HttpStatusCodeResultpara definir o HttpContext.Response.StatusCodee, HttpContext.Response.StatusDescriptionmas, em seguida base.ExecuteResult(context), renderizarei a visualização adequada porque, novamente, derivei de ViewResult. Simples o suficiente, não é? Espero que isso seja implementado no núcleo MVC.

Veja meu BaseControllerabaixo:

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 em sua ação como esta:

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

E em _Layout.cshtml (como a página mestra)

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

Além disso, você pode usar uma visão personalizada Error.shtmlou criar uma nova NotFound.cshtmlcomo comentei no código e pode definir um modelo de visão para a descrição do status e outras explicações.

CallMeLaNN
fonte
Você sempre pode registrar um filtro global que bate um controlador de base porque você tem que LEMBRAR de usar seu controlador de base!
John Culviner
:) Não tenho certeza se isso ainda é um problema no MVC4. O que quero dizer naquele momento é o filtro HandleNotFoundAttribute respondido por outra pessoa. Não é necessário ser aplicado para cada ação. Por exemplo, é adequado apenas para ações que tenham id param, mas não a ação Index (). Concordei com o filtro global, não para HandleNotFoundAttribute, mas para um HandleErrorAttribute personalizado.
CallMeLaNN
Achei que MVC3 também tivesse, não tenho certeza. Boa discussão, independentemente para outros que possam encontrar a resposta
John Culviner