asp.net-mvc - mvc - tag helpers asp net core 2




¿Cómo puedo manejar adecuadamente 404 en ASP.NET MVC? (13)

Requisitos para 404

Los siguientes son mis requisitos para una solución 404 y a continuación muestro cómo lo implemento:

  • Quiero manejar rutas emparejadas con malas acciones.
  • Quiero manejar rutas emparejadas con malos controladores
  • Quiero manejar rutas no coincidentes (URLs arbitrarias que mi aplicación no puede entender): no quiero que estas se conviertan en Global.asax o IIS porque no puedo redirigir nuevamente a mi aplicación MVC correctamente
  • Quiero una forma de manejar de la misma manera que antes, 404 personalizada, como cuando se envía una ID para un objeto que no existe (tal vez se elimine)
  • Quiero que todos mis 404 devuelvan una vista MVC (no una página estática) a la que pueda enviar más datos más tarde si es necesario ( codinghorror.com/blog/2007/03/… ) y deben devolver el código de estado HTTP 404

Solución

Creo que deberías guardar Application_Error en el Global.asax para cosas más altas, como las excepciones no controladas y el registro (como muestra la respuesta de Shay Jacoby ) pero no el manejo 404. Esta es la razón por la que mi sugerencia mantiene las cosas 404 fuera del archivo Global.asax.

Paso 1: tener un lugar común para la lógica de error 404

Esta es una buena idea para el mantenimiento. Utilice un ErrorController para que las mejoras futuras de su codinghorror.com/blog/2007/03/… puedan adaptarse fácilmente. Además, ¡ asegúrate de que tu respuesta tenga el código 404 !

public class ErrorController : MyController
{
    #region Http404

    public ActionResult Http404(string url)
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        var model = new NotFoundViewModel();
        // If the url is relative ('NotFound' route) then replace with Requested path
        model.RequestedUrl = Request.Url.OriginalString.Contains(url) & Request.Url.OriginalString != url ?
            Request.Url.OriginalString : url;
        // Dont get the user stuck in a 'retry loop' by
        // allowing the Referrer to be the same as the Request
        model.ReferrerUrl = Request.UrlReferrer != null &&
            Request.UrlReferrer.OriginalString != model.RequestedUrl ?
            Request.UrlReferrer.OriginalString : null;

        // TODO: insert ILogger here

        return View("NotFound", model);
    }
    public class NotFoundViewModel
    {
        public string RequestedUrl { get; set; }
        public string ReferrerUrl { get; set; }
    }

    #endregion
}

Paso 2: use una clase de controlador base para poder invocar fácilmente su acción 404 personalizada y conectar HandleUnknownAction

Los 404 en ASP.NET MVC deben capturarse en varios lugares. El primero es HandleUnknownAction .

El método InvokeHttp404 crea un lugar común para redireccionar al ErrorController y nuestra nueva acción Http404 . ¡Piense DRY !

public abstract class MyController : Controller
{
    #region Http404 handling

    protected override void HandleUnknownAction(string actionName)
    {
        // If controller is ErrorController dont 'nest' exceptions
        if (this.GetType() != typeof(ErrorController))
            this.InvokeHttp404(HttpContext);
    }

    public ActionResult InvokeHttp404(HttpContextBase httpContext)
    {
        IController errorController = ObjectFactory.GetInstance<ErrorController>();
        var errorRoute = new RouteData();
        errorRoute.Values.Add("controller", "Error");
        errorRoute.Values.Add("action", "Http404");
        errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
        errorController.Execute(new RequestContext(
             httpContext, errorRoute));

        return new EmptyResult();
    }

    #endregion
}

Paso 3: Use la inyección de dependencia en su fábrica de controladores y conecte 404 HttpExceptions

Al igual que (no tiene que ser StructureMap):

MVC1.0 ejemplo:

public class StructureMapControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(controllerType);
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
            {
                IController errorController = ObjectFactory.GetInstance<ErrorController>();
                ((ErrorController)errorController).InvokeHttp404(RequestContext.HttpContext);

                return errorController;
            }
            else
                throw ex;
        }

        return ObjectFactory.GetInstance(controllerType) as Controller;
    }
}

Ejemplo MVC2.0:

    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(requestContext, controllerType);
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == 404)
            {
                IController errorController = ObjectFactory.GetInstance<ErrorController>();
                ((ErrorController)errorController).InvokeHttp404(requestContext.HttpContext);

                return errorController;
            }
            else
                throw ex;
        }

        return ObjectFactory.GetInstance(controllerType) as Controller;
    }

Creo que es mejor detectar errores más cerca de donde se originan. Es por eso que prefiero lo anterior al controlador Application_Error .

Este es el segundo lugar para atrapar 404s.

Paso 4: agregue una ruta NotFound a Global.asax para las URL que no se pueden analizar en su aplicación

Esta ruta debe apuntar a nuestra acción Http404 . Observe que el parámetro url será un URL relativo porque el motor de enrutamiento está eliminando la parte del dominio aquí. Es por eso que tenemos toda esa lógica de url condicional en el Paso 1.

        routes.MapRoute("NotFound", "{*url}", 
            new { controller = "Error", action = "Http404" });

Este es el tercer y último lugar para capturar 404s en una aplicación MVC que no invoca usted mismo. Si no encuentra rutas inigualables aquí, MVC pasará el problema a ASP.NET (Global.asax) y realmente no quiere eso en esta situación.

Paso 5: Finalmente, invoca 404s cuando tu aplicación no pueda encontrar algo

Al igual que cuando se envía una identificación MyController a mi controlador de préstamos (deriva de MyController ):

    //
    // GET: /Detail/ID

    public ActionResult Detail(int ID)
    {
        Loan loan = this._svc.GetLoans().WithID(ID);
        if (loan == null)
            return this.InvokeHttp404(HttpContext);
        else
            return View(loan);
    }

Sería bueno si todo esto se pudiera conectar a menos lugares con menos código, pero creo que esta solución es más fácil de mantener, verificable y bastante pragmática.

Gracias por los comentarios hasta ahora. Me encantaría conseguir más.

NOTA: Esto ha sido editado significativamente desde mi respuesta original, pero el propósito / los requisitos son los mismos, es por eso que no he agregado una nueva respuesta

Estoy usando RC2

Uso de enrutamiento de URL:

routes.MapRoute(
    "Error",
     "{*url}",
     new { controller = "Errors", action = "NotFound" }  // 404s
);

Lo anterior parece atender solicitudes como esta (asumiendo que las tablas de ruta predeterminadas están configuradas por el proyecto MVC inicial): "/ blah / blah / blah / blah"

Anulando HandleUnknownAction () en el propio controlador:

// 404s - handle here (bad action requested
protected override void HandleUnknownAction(string actionName) {
    ViewData["actionName"] = actionName;
    View("NotFound").ExecuteResult(this.ControllerContext);
}  

Sin embargo, las estrategias anteriores no manejan una solicitud a un controlador incorrecto / desconocido. Por ejemplo, no tengo un "/ IDoNotExist", si lo solicito, obtengo la página 404 genérica del servidor web y no mi 404 si uso enrutamiento + anulación.

Entonces, finalmente, mi pregunta es: ¿hay alguna forma de detectar este tipo de solicitud utilizando una ruta o alguna otra cosa en el marco MVC?

O ¿debería simplemente usar Web.Config customErrors como mi controlador 404 y olvidarme de todo esto? Supongo que si voy con customErrors tendré que almacenar la página 404 genérica fuera de / Views debido a las restricciones de Web.Config en el acceso directo.


Respuesta rápida / TL; DR

Para la gente perezosa por ahí:

Install-Package MagicalUnicornMvcErrorToolkit -Version 1.0

A continuación, elimine esta línea de global.asax

GlobalFilters.Filters.Add(new HandleErrorAttribute());

Y esto es solo para IIS7 + e IIS Express.

Si estás usando Cassini ... bueno ... um ... er ... incómodo ...

Respuesta larga y explicada

Sé que esto ha sido respondido. Pero la respuesta es REALMENTE SENCILLA (saluda a David Fowler y Damian Edwards por responder realmente a esto).

No hay necesidad de hacer nada personalizado .

Para ASP.NET MVC3 , todos los bits y piezas están ahí.

Paso 1 -> Actualiza tu web.config en DOS lugares.

<system.web>
    <customErrors mode="On" defaultRedirect="/ServerError">
      <error statusCode="404" redirect="/NotFound" />
    </customErrors>

y

<system.webServer>
    <httpErrors errorMode="Custom">
      <remove statusCode="404" subStatusCode="-1" />
      <error statusCode="404" path="/NotFound" responseMode="ExecuteURL" />
      <remove statusCode="500" subStatusCode="-1" />
      <error statusCode="500" path="/ServerError" responseMode="ExecuteURL" />
    </httpErrors>    

...
<system.webServer>
...
</system.web>

Ahora tome nota de las RUTAS que he decidido usar. Puedes usar cualquier cosa, pero mis rutas son

  • /NotFound <- para un 404 no encontrado, página de error.
  • /ServerError <- para cualquier otro error, incluya los errores que ocurren en mi código. este es un error interno del servidor 500

¿Ve cómo la primera sección en <system.web> solo tiene una entrada personalizada? La entrada statusCode="404" ? Solo he enumerado un código de estado porque todos los demás errores, incluido el 500 Server Error (es decir, esos molestos errores que ocurren cuando su código tiene un error y bloquea la solicitud del usuario) .. todos los demás errores son manejados por la configuración defaultRedirect="/ServerError" .. que dice, si usted no es una página 404 no encontrada, entonces vaya a la ruta /ServerError .

De acuerdo. eso está fuera del camino ... ahora a mis rutas listadas en global.asax

Paso 2 - Creando las rutas en Global.asax

Aquí está mi sección de ruta completa ...

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.IgnoreRoute("{*favicon}", new {favicon = @"(.*/)?favicon.ico(/.*)?"});

    routes.MapRoute(
        "Error - 404",
        "NotFound",
        new { controller = "Error", action = "NotFound" }
        );

    routes.MapRoute(
        "Error - 500",
        "ServerError",
        new { controller = "Error", action = "ServerError"}
        );

    routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new {controller = "Home", action = "Index", id = UrlParameter.Optional}
        );
}

Eso enumera dos rutas de ignorar -> axd's y favicons (ooo! Bonus ignore route, ¡para usted!) Luego (y el orden es IMPERATIVO AQUÍ), tengo mis dos rutas de manejo de errores explícitos ... seguidas de cualquier otra ruta. En este caso, el predeterminado. Por supuesto, tengo más, pero eso es especial para mi sitio web. Solo asegúrese de que las rutas de error estén en la parte superior de la lista. El orden es imperativo .

Finalmente, mientras estamos dentro de nuestro archivo global.asax , NO registramos globalmente el atributo HandleError. No, no, no señor. Nadda. No Nien Negativo. Noooooooooo ...

Eliminar esta línea de global.asax

GlobalFilters.Filters.Add(new HandleErrorAttribute());

Paso 3 - Crea el controlador con los métodos de acción.

Ahora ... agregamos un controlador con dos métodos de acción ...

public class ErrorController : Controller
{
    public ActionResult NotFound()
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        return View();
    }

    public ActionResult ServerError()
    {
        Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        // Todo: Pass the exception into the view model, which you can make.
        //       That's an exercise, dear reader, for -you-.
        //       In case u want to pass it to the view, if you're admin, etc.
        // if (User.IsAdmin) // <-- I just made that up :) U get the idea...
        // {
        //     var exception = Server.GetLastError();
        //     // etc..
        // }

        return View();
    }

    // Shhh .. secret test method .. ooOOooOooOOOooohhhhhhhh
    public ActionResult ThrowError()
    {
        throw new NotImplementedException("Pew ^ Pew");
    }
}

Ok, vamos a ver esto. En primer lugar, no hay [HandleError] atributo [HandleError] aquí. ¿Por qué? Debido a que el marco ASP.NET incorporado ya está manejando errores Y hemos especificado toda la mierda que necesitamos hacer para manejar un error :) ¡Está en este método!

A continuación, tengo los dos métodos de acción. Nada duro allí. Si desea mostrar cualquier información de excepción, entonces puede usar Server.GetLastError() para obtener esa información.

Bonificación WTF: Sí, hice un tercer método de acción, para probar el manejo de errores.

Paso 4 - Crea las Vistas

Y por último, crea dos vistas. Ponga em en el punto de vista normal, para este controlador.

Comentarios extra

  • No necesita un Application_Error(object sender, EventArgs e)
  • Los pasos anteriores todos funcionan perfectamente al 100% con Elmah . Elmah deshaciéndose wroxs!

Y eso, amigos míos, debería ser eso.

Ahora, ¡felicidades por leer esto y tener un Unicornio como premio!


Aquí hay otro método que utiliza herramientas MVC que puede manejar solicitudes de nombres de controladores incorrectos, nombres de rutas incorrectos y cualquier otro criterio que considere adecuado dentro de un método de Acción. Personalmente, prefiero evitar la mayor cantidad de configuraciones de web.config que sea posible, ya que hacen la redirección Server.Transfer y no son compatibles con ResponseRewrite ( Server.Transfer ) usando vistas de Razor. Prefiero devolver un 404 con una página de error personalizada por razones de SEO.

Algo de esto es una nueva versión de la técnica de cottsak anterior.

Esta solución también usa configuraciones mínimas de web.config que prefieren los filtros de error MVC 3 en su lugar.

Uso

Simplemente lance una HttpException desde una acción o ActionFilterAttribute personalizado.

Throw New HttpException(HttpStatusCode.NotFound, "[Custom Exception Message Here]")

Paso 1

Agregue la siguiente configuración a su web.config. Esto es necesario para usar HandleErrorAttribute de MVC.

<customErrors mode="On" redirectMode="ResponseRedirect" />

Paso 2

Agregue un HandleHttpErrorAttribute personalizado al HandleErrorAttribute del marco MVC, excepto por los errores de HTTP:

<AttributeUsage(AttributeTargets.All, AllowMultiple:=True)>
Public Class HandleHttpErrorAttribute
    Inherits FilterAttribute
    Implements IExceptionFilter

    Private Const m_DefaultViewFormat As String = "ErrorHttp{0}"

    Private m_HttpCode As HttpStatusCode
    Private m_Master As String
    Private m_View As String

    Public Property HttpCode As HttpStatusCode
        Get
            If m_HttpCode = 0 Then
                Return HttpStatusCode.NotFound
            End If
            Return m_HttpCode
        End Get
        Set(value As HttpStatusCode)
            m_HttpCode = value
        End Set
    End Property

    Public Property Master As String
        Get
            Return If(m_Master, String.Empty)
        End Get
        Set(value As String)
            m_Master = value
        End Set
    End Property

    Public Property View As String
        Get
            If String.IsNullOrEmpty(m_View) Then
                Return String.Format(m_DefaultViewFormat, Me.HttpCode)
            End If
            Return m_View
        End Get
        Set(value As String)
            m_View = value
        End Set
    End Property

    Public Sub OnException(filterContext As System.Web.Mvc.ExceptionContext) Implements System.Web.Mvc.IExceptionFilter.OnException
        If filterContext Is Nothing Then Throw New ArgumentException("filterContext")

        If filterContext.IsChildAction Then
            Return
        End If

        If filterContext.ExceptionHandled OrElse Not filterContext.HttpContext.IsCustomErrorEnabled Then
            Return
        End If

        Dim ex As HttpException = TryCast(filterContext.Exception, HttpException)
        If ex Is Nothing OrElse ex.GetHttpCode = HttpStatusCode.InternalServerError Then
            Return
        End If

        If ex.GetHttpCode <> Me.HttpCode Then
            Return
        End If

        Dim controllerName As String = filterContext.RouteData.Values("controller")
        Dim actionName As String = filterContext.RouteData.Values("action")
        Dim model As New HandleErrorInfo(filterContext.Exception, controllerName, actionName)

        filterContext.Result = New ViewResult With {
            .ViewName = Me.View,
            .MasterName = Me.Master,
            .ViewData = New ViewDataDictionary(Of HandleErrorInfo)(model),
            .TempData = filterContext.Controller.TempData
        }
        filterContext.ExceptionHandled = True
        filterContext.HttpContext.Response.Clear()
        filterContext.HttpContext.Response.StatusCode = Me.HttpCode
        filterContext.HttpContext.Response.TrySkipIisCustomErrors = True
    End Sub
End Class

Paso 3

Agregue filtros a GlobalFilterCollection ( GlobalFilters.Filters ) en Global.asax . Este ejemplo Views/Shared/Error.vbhtml todos los errores InternalServerError (500) a la vista compartida Error ( Views/Shared/Error.vbhtml ). Los errores NotFound (404) se enviarán también a ErrorHttp404.vbhtml en las vistas compartidas. He agregado un error 401 aquí para mostrarle cómo se puede extender esto para códigos de error HTTP adicionales. Tenga en cuenta que estas vistas deben ser compartidas y que todas usan el objeto System.Web.Mvc.HandleErrorInfo como modelo.

filters.Add(New HandleHttpErrorAttribute With {.View = "ErrorHttp401", .HttpCode = HttpStatusCode.Unauthorized})
filters.Add(New HandleHttpErrorAttribute With {.View = "ErrorHttp404", .HttpCode = HttpStatusCode.NotFound})
filters.Add(New HandleErrorAttribute With {.View = "Error"})

Etapa 4

Cree una clase de controlador base y herede de ella en sus controladores. Este paso nos permite manejar nombres de acción desconocidos y elevar el error HTTP 404 a nuestro HandleHttpErrorAttribute.

Public Class BaseController
    Inherits System.Web.Mvc.Controller

    Protected Overrides Sub HandleUnknownAction(actionName As String)
        Me.ActionInvoker.InvokeAction(Me.ControllerContext, "Unknown")
    End Sub

    Public Function Unknown() As ActionResult
        Throw New HttpException(HttpStatusCode.NotFound, "The specified controller or action does not exist.")
        Return New EmptyResult
    End Function
End Class

Paso 5

Cree una anulación de ControllerFactory y anúltela en su archivo Global.asax en Application_Start. Este paso nos permite elevar la excepción HTTP 404 cuando se ha especificado un nombre de controlador no válido.

Public Class MyControllerFactory
    Inherits DefaultControllerFactory

    Protected Overrides Function GetControllerInstance(requestContext As System.Web.Routing.RequestContext, controllerType As System.Type) As System.Web.Mvc.IController
        Try
            Return MyBase.GetControllerInstance(requestContext, controllerType)
        Catch ex As HttpException
            Return DependencyResolver.Current.GetService(Of BaseController)()
        End Try
    End Function
End Class

'In Global.asax.vb Application_Start:

controllerBuilder.Current.SetControllerFactory(New MyControllerFactory)

Paso 6

Incluya una ruta especial en su RoutTable.Routes para la acción Desconocido BaseController. Esto nos ayudará a generar un 404 en el caso de que un usuario acceda a un controlador desconocido o una acción desconocida.

'BaseController
routes.MapRoute( _
    "Unknown", "BaseController/{action}/{id}", _
    New With {.controller = "BaseController", .action = "Unknown", .id = UrlParameter.Optional} _
)

Resumen

Este ejemplo demostró cómo se puede usar el marco MVC para devolver códigos de error de HTTP 404 al navegador sin una redirección utilizando atributos de filtro y vistas de error compartidas. También demuestra que muestra la misma página de error personalizada cuando se especifican nombres de controlador y de acción no válidos.

Agregaré una captura de pantalla de un nombre de controlador no válido, un nombre de acción y un 404 personalizado generado desde la acción Home / TriggerNotFound si obtengo suficientes votos para publicar un =) Fiddler devuelve un mensaje 404 cuando accedo a las siguientes URL utilizando esta solución:

/InvalidController
/Home/InvalidRoute
/InvalidController/InvalidRoute
/Home/TriggerNotFound

El post de cottsak arriba y estos artículos fueron buenas referencias.


El código está tomado de http://blogs.microsoft.co.il/blogs/shay/archive/2009/03/06/real-world-error-hadnling-in-asp-net-mvc-rc2.aspx y funciona en ASP.net MVC 1.0 también

Así es como manejo las excepciones de http:

protected void Application_Error(object sender, EventArgs e)
{
   Exception exception = Server.GetLastError();
   // Log the exception.

   ILogger logger = Container.Resolve<ILogger>();
   logger.Error(exception);

   Response.Clear();

   HttpException httpException = exception as HttpException;

   RouteData routeData = new RouteData();
   routeData.Values.Add("controller", "Error");

   if (httpException == null)
   {
       routeData.Values.Add("action", "Index");
   }
   else //It's an Http Exception, Let's handle it.
   {
       switch (httpException.GetHttpCode())
       {
          case 404:
              // Page not found.
              routeData.Values.Add("action", "HttpError404");
              break;
          case 500:
              // Server error.
              routeData.Values.Add("action", "HttpError500");
              break;

           // Here you can handle Views to other error codes.
           // I choose a General error template  
           default:
              routeData.Values.Add("action", "General");
              break;
      }
  }           

  // Pass exception details to the target error View.
  routeData.Values.Add("error", exception);

  // Clear the error on server.
  Server.ClearError();

  // Avoid IIS7 getting in the middle
  Response.TrySkipIisCustomErrors = true; 

  // Call target Controller and pass the routeData.
  IController errorController = new ErrorController();
  errorController.Execute(new RequestContext(    
       new HttpContextWrapper(Context), routeData));
}

La única forma en que podía conseguir que el método de @ cottsak funcionara con los controladores no válidos era modificar la solicitud de ruta existente en el CustomControllerFactory, así:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(requestContext, controllerType); 
            else
                return ObjectFactory.GetInstance(controllerType) as Controller;
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
            {
                requestContext.RouteData.Values["controller"] = "Error";
                requestContext.RouteData.Values["action"] = "Http404";
                requestContext.RouteData.Values.Add("url", requestContext.HttpContext.Request.Url.OriginalString);

                return ObjectFactory.GetInstance<ErrorController>();
            }
            else
                throw ex;
        }
    }
}

Debo mencionar que estoy usando MVC 2.0.


Realmente me gusta la solución de cottsaks y creo que se explica muy claramente. Mi única adición fue alterar el paso 2 como sigue

public abstract class MyController : Controller
{

    #region Http404 handling

    protected override void HandleUnknownAction(string actionName)
    {
        //if controller is ErrorController dont 'nest' exceptions
        if(this.GetType() != typeof(ErrorController))
        this.InvokeHttp404(HttpContext);
    }

    public ActionResult InvokeHttp404(HttpContextBase httpContext)
    {
        IController errorController = ObjectFactory.GetInstance<ErrorController>();
        var errorRoute = new RouteData();
        errorRoute.Values.Add("controller", "Error");
        errorRoute.Values.Add("action", "Http404");
        errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
        errorController.Execute(new RequestContext(
             httpContext, errorRoute));

        return new EmptyResult();
    }

    #endregion
}

Básicamente, esto impide que las URL que contienen acciones Y controladores no válidos activen la rutina de excepción dos veces. por ejemplo, para urls como asdfsdf / dfgdfgd


Agregando mi solución, que es casi idéntica a la de Herman Kan's, con una pequeña arruga para permitir que funcione en mi proyecto.

Crear un controlador de error personalizado:

public class Error404Controller : BaseController
{
    [HttpGet]
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View("404");
    }
}

Luego crea una fábrica de controladores personalizados:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        return controllerType == null ? new Error404Controller() : base.GetControllerInstance(requestContext, controllerType);
    }
}

Finalmente, agregue un reemplazo al controlador de error personalizado:

protected override void HandleUnknownAction(string actionName)
{
    var errorRoute = new RouteData();
    errorRoute.Values.Add("controller", "Error404");
    errorRoute.Values.Add("action", "PageNotFound");
    new Error404Controller().Execute(new RequestContext(HttpContext, errorRoute));
}

Y eso es. No hay necesidad de cambios web.config.


En MVC4, WebAPI 404 se puede manejar de la siguiente manera,

CURSOS DE APICONTROLADOR

    // GET /api/courses/5
    public HttpResponseMessage<Courses> Get(int id)
    {
        HttpResponseMessage<Courses> resp = null;

        var aCourse = _courses.Where(c => c.Id == id).FirstOrDefault();

        resp = aCourse == null ? new HttpResponseMessage<Courses>(System.Net.HttpStatusCode.NotFound) : new HttpResponseMessage<Courses>(aCourse);

        return resp;
    }

CONTROLADOR DE VIVIENDA

public ActionResult Course(int id)
{
    return View(id);
}

VER

<div id="course"></div>
<script type="text/javascript">
    var id = @Model;
    var course = $('#course');
    $.ajax({    
        url: '/api/courses/' + id,
        success: function (data) {
            course.text(data.Name);
        },
        statusCode: {
            404: function() 
            {
                course.text('Course not available!');    
            }
        }
    });
</script>

GLOBAL

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

RESULTADOS


Mi solución abreviada que funciona con áreas no controladas, controladores y acciones:

  1. Crear una vista 404.cshtml.

  2. Crea una clase base para tus controladores:

    public class Controller : System.Web.Mvc.Controller
    {
        protected override void HandleUnknownAction(string actionName)
        {
            Http404().ExecuteResult(ControllerContext);
        }
    
        protected virtual ViewResult Http404()
        {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return View("404");
        }
    }
    
  3. Cree una fábrica de controladores personalizados que devuelva su controlador base como una alternativa:

    public class ControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType != null)
                return base.GetControllerInstance(requestContext, controllerType);
    
            return new Controller();
        }
    }
    
  4. Añadir a Application_Start()la siguiente línea:

    ControllerBuilder.Current.SetControllerFactory(typeof(ControllerFactory));
    

Mi solución, por si alguien la encuentra útil.

En Web.config:

<system.web>
    <customErrors mode="On" defaultRedirect="Error" >
      <error statusCode="404" redirect="~/Error/PageNotFound"/>
    </customErrors>
    ...
</system.web>

En Controllers/ErrorController.cs:

public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        if(Request.IsAjaxRequest()) {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return Content("Not Found", "text/plain");
        }

        return View();
    }
}

Añadir un PageNotFound.cshtmlen la Sharedcarpeta, y eso es todo.


Me parece que la CustomErrorsconfiguración estándar debería funcionar sin embargo, debido a la confianza en Server.Transferella, parece que la implementación interna de ResponseRewriteno es compatible con MVC.

Esto me parece un agujero de funcionalidad deslumbrante, así que decidí volver a implementar esta característica utilizando un módulo HTTP. La solución a continuación le permite manejar cualquier código de estado HTTP (incluido 404) al redirigir a cualquier ruta MVC válida como lo haría normalmente.

<customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
    <error statusCode="404" redirect="404.aspx" />
    <error statusCode="500" redirect="~/MVCErrorPage" />
</customErrors>

Esto ha sido probado en las siguientes plataformas;

  • MVC4 en modo de tubería integrada (IIS Express 8)
  • MVC4 en modo clásico (Servidor de desarrollo VS, Cassini)
  • MVC4 en modo clásico (IIS6)

Beneficios

  • Solución genérica que se puede colocar en cualquier proyecto MVC.
  • Habilita la compatibilidad con la configuración tradicional de errores personalizados.
  • Funciona tanto en modo integrado como en modo clásico.

La solución

namespace Foo.Bar.Modules {

    /// <summary>
    /// Enables support for CustomErrors ResponseRewrite mode in MVC.
    /// </summary>
    public class ErrorHandler : IHttpModule {

        private HttpContext HttpContext { get { return HttpContext.Current; } }
        private CustomErrorsSection CustomErrors { get; set; }

        public void Init(HttpApplication application) {
            System.Configuration.Configuration configuration = WebConfigurationManager.OpenWebConfiguration("~");
            CustomErrors = (CustomErrorsSection)configuration.GetSection("system.web/customErrors");

            application.EndRequest += Application_EndRequest;
        }

        protected void Application_EndRequest(object sender, EventArgs e) {

            // only handle rewrite mode, ignore redirect configuration (if it ain't broke don't re-implement it)
            if (CustomErrors.RedirectMode == CustomErrorsRedirectMode.ResponseRewrite && HttpContext.IsCustomErrorEnabled) {

                int statusCode = HttpContext.Response.StatusCode;

                // if this request has thrown an exception then find the real status code
                Exception exception = HttpContext.Error;
                if (exception != null) {
                    // set default error status code for application exceptions
                    statusCode = (int)HttpStatusCode.InternalServerError;
                }

                HttpException httpException = exception as HttpException;
                if (httpException != null) {
                    statusCode = httpException.GetHttpCode();
                }

                if ((HttpStatusCode)statusCode != HttpStatusCode.OK) {

                    Dictionary<int, string> errorPaths = new Dictionary<int, string>();

                    foreach (CustomError error in CustomErrors.Errors) {
                        errorPaths.Add(error.StatusCode, error.Redirect);
                    }

                    // find a custom error path for this status code
                    if (errorPaths.Keys.Contains(statusCode)) {
                        string url = errorPaths[statusCode];

                        // avoid circular redirects
                        if (!HttpContext.Request.Url.AbsolutePath.Equals(VirtualPathUtility.ToAbsolute(url))) {

                            HttpContext.Response.Clear();
                            HttpContext.Response.TrySkipIisCustomErrors = true;

                            HttpContext.Server.ClearError();

                            // do the redirect here
                            if (HttpRuntime.UsingIntegratedPipeline) {
                                HttpContext.Server.TransferRequest(url, true);
                            }
                            else {
                                HttpContext.RewritePath(url, false);

                                IHttpHandler httpHandler = new MvcHttpHandler();
                                httpHandler.ProcessRequest(HttpContext);
                            }

                            // return the original status code to the client
                            // (this won't work in integrated pipleline mode)
                            HttpContext.Response.StatusCode = statusCode;

                        }
                    }

                }

            }

        }

        public void Dispose() {

        }


    }

}

Uso

Incluya esto como el último módulo HTTP en su web.config

  <system.web>
    <httpModules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </httpModules>
  </system.web>

  <!-- IIS7+ -->
  <system.webServer>
    <modules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </modules>
  </system.webServer>

Para aquellos que prestan atención, notarán que en el modo de tubería integrada esto siempre responderá con HTTP 200 debido a la forma en que Server.TransferRequestfunciona. Para devolver el código de error correcto, uso el siguiente controlador de error.

public class ErrorController : Controller {

    public ErrorController() { }

    public ActionResult Index(int id) {
        // pass real error code to client
        HttpContext.Response.StatusCode = id;
        HttpContext.Response.TrySkipIisCustomErrors = true;

        return View("Errors/" + id.ToString());
    }

}

Pasé por la mayoría de las soluciones publicadas en este hilo. Si bien esta pregunta puede ser antigua, aún es muy aplicable a los nuevos proyectos incluso ahora, así que dediqué bastante tiempo a leer las respuestas que se presentan aquí, así como a dónde.

Como @Marco señaló los diferentes casos en los que puede suceder un 404, verifiqué la solución que compilé junto con esa lista. Además de su lista de requisitos, también agregué uno más.

  • La solución debe poder manejar MVC, así como las llamadas AJAX / WebAPI de la manera más apropiada. (es decir, si 404 sucede en MVC, debería mostrar la página No encontrado y si 404 ocurre en WebAPI, no debería secuestrar la respuesta XML / JSON para que el Javascript que consume puede analizarla fácilmente)

Esta solución es 2 veces:

La primera parte proviene de @Guillaume en https://.com/a/27354140/2310818 . Su solución se encarga de cualquier 404 que haya sido causado debido a una ruta no válida, un controlador no válido y una acción no válida.

La idea es crear un formulario web y luego hacer que llame a la acción NotFound de su controlador de errores MVC. Hace todo esto sin ninguna redirección, por lo que no verá un solo 302 en Fiddler. La URL original también se conserva, lo que hace que esta solución sea fantástica.

La segunda parte proviene de @ Germán en https://.com/a/5536676/2310818 . ¡Su solución se encarga de cualquier 404 devuelto por sus acciones en la forma de HttpNotFoundResult () o arroja una nueva HttpException ()!

La idea es tener un filtro que mire la respuesta, así como la excepción lanzada por sus controladores MVC y llamar la acción apropiada en su controlador de errores. Una vez más, esta solución funciona sin ningún redireccionamiento y la URL original se conserva.

Como puede ver, ambas soluciones juntas ofrecen un mecanismo de manejo de errores muy robusto y cumplen todos los requisitos enumerados por @Marco, así como mis requisitos. Si desea ver una muestra de trabajo o una demostración de esta solución, déjelo en los comentarios y me complacerá hacerlo.


Tratar con los errores en ASP.NET MVC es solo un dolor en el trasero. Intenté muchas sugerencias en esta página y en otras preguntas y sitios, y nada funciona bien. Una sugerencia fue manejar los errores en web.config dentro de system.webserver pero eso solo devuelve páginas en blanco .

Mi objetivo al llegar a esta solución era:

  • NO REDIRECTA
  • Devuelva los CÓDIGOS DE ESTADO ADECUADOS no 200 / Ok como el manejo de errores predeterminado

Aquí está mi solución.

1. Agregue lo siguiente a la sección system.web

   <system.web>
     <customErrors mode="On" redirectMode="ResponseRewrite">
      <error statusCode="404"  redirect="~/Error/404.aspx" />
      <error statusCode="500" redirect="~/Error/500.aspx" />
     </customErrors>
    <system.web>

Lo anterior maneja las direcciones URL no manejadas por route.config y las excepciones no manejadas, especialmente las encontradas en las vistas. Tenga en cuenta que he utilizado aspx no html . Esto es para que pueda agregar un código de respuesta en el código que está detrás.

2 .Cree una carpeta llamada Error (o lo que prefiera) en la raíz de su proyecto y agregue los dos formularios web. A continuación se muestra mi página 404;

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="404.aspx.cs" Inherits="Myapp.Error._404" %>

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title >Page Not found</title>
    <link href="<%=ResolveUrl("~/Content/myapp.css")%>" rel="stylesheet" />
</head>
<body>
    <div class="top-nav">
      <a runat="server" class="company-logo" href="~/"></a>
    </div>
    <div>
        <h1>404 - Page Not found</h1>
        <p>The page you are looking for cannot be found.</p>
        <hr />
        <footer></footer>
    </div>
</body>
</html>

Y en el código de detrás puse el código de respuesta

protected void Page_Load(object sender, EventArgs e)
{
    Response.StatusCode = 404;
}

Haz lo mismo para la página 500.

3. Para manejar los errores dentro de los controladores. Hay muchas maneras de hacerlo. Esto es lo que funcionó para mí.Todos mis controladores heredan de un controlador base. En el controlador base, tengo los siguientes métodos

protected ActionResult ShowNotFound()
{
    return ShowNotFound("Page not found....");
}

protected ActionResult ShowNotFound(string message)
{
    return ShowCustomError(HttpStatusCode.NotFound, message);
}

protected ActionResult ShowServerError()
{
    return ShowServerError("Application error....");
}

protected ActionResult ShowServerError(string message)
{
    return ShowCustomError(HttpStatusCode.InternalServerError, message);
}

protected ActionResult ShowNotAuthorized()
{
    return ShowNotAuthorized("You are not allowed ....");

}

protected ActionResult ShowNotAuthorized(string message)
{
    return ShowCustomError(HttpStatusCode.Forbidden, message);
}

protected ActionResult ShowCustomError(HttpStatusCode statusCode, string message)
{
    Response.StatusCode = (int)statusCode;
    string title = "";
    switch (statusCode)
    {
        case HttpStatusCode.NotFound:
            title = "404 - Not found";
            break;
        case HttpStatusCode.Forbidden:
            title = "403 - Access Denied";
            break;
        default:
            title = "500 - Application Error";
            break;
    }
    ViewBag.Title = title;
    ViewBag.Message = message;
    return View("CustomError");
}

4. Agregue el CustomError.cshtml a su carpeta de vistas compartidas . Abajo está el mío;

<h1>@ViewBag.Title</h1>
<br />
<p>@ViewBag.Message</p>

Ahora en tu controlador de aplicación puedes hacer algo como esto;

public class WidgetsController : ControllerBase
{
  [HttpGet]
  public ActionResult Edit(int id)
  {
    Try
    {
       var widget = db.getWidgetById(id);
       if(widget == null)
          return ShowNotFound();
          //or return ShowNotFound("Invalid widget!");
       return View(widget);
    }
    catch(Exception ex)
    {
       //log error
       logger.Error(ex)
       return ShowServerError();
    }
  }
}

Ahora para la advertencia . No manejará errores de archivos estáticos. Entonces, si tiene una ruta como example.com/widgets y el usuario la cambia a example.com/widgets.html , obtendrán la página de error predeterminada de IIS, por lo que tendrá que manejar los errores de nivel de IIS de otra manera.





http-status-code-404