for - ¿Cómo puedo manejar adecuadamente 404 en ASP.NET MVC?
tag helper asp net core 2 (19)
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.
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
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!
ASP.NET MVC no soporta páginas 404 personalizadas muy bien. Fábrica de controladores personalizados, ruta general, clase de controlador base con HandleUnknownAction
- argh!
Las páginas de error personalizadas de IIS son una mejor alternativa hasta ahora
web.config
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="404" />
<error statusCode="404" responseMode="ExecuteURL" path="/Error/PageNotFound" />
</httpErrors>
</system.webServer>
Controlador de errores
public class ErrorController : Controller
{
public ActionResult PageNotFound()
{
Response.StatusCode = 404;
return View();
}
}
Proyecto de muestra
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));
}
He investigado MUCHO cómo administrar 404 correctamente en MVC (específicamente MVC3) , y esta, en mi humilde opinión, es la mejor solución que he encontrado:
En global.asax:
public class MvcApplication : HttpApplication
{
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == 404)
{
Response.Clear();
var rd = new RouteData();
rd.DataTokens["area"] = "AreaName"; // In case controller is in another area
rd.Values["controller"] = "Errors";
rd.Values["action"] = "NotFound";
IController c = new ErrorsController();
c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
}
}
}
Controlador de errores:
public sealed class ErrorsController : Controller
{
public ActionResult NotFound()
{
ActionResult result;
object model = Request.Url.PathAndQuery;
if (!Request.IsAjaxRequest())
result = View(model);
else
result = PartialView("_NotFound", model);
return result;
}
}
(Opcional)
Explicación:
AFAIK, hay 6 casos diferentes en los que una aplicación ASP.NET MVC3 puede generar 404s.
(Generado automáticamente por ASP.NET Framework :)
(1) Una URL no encuentra una coincidencia en la tabla de rutas.
(Generado automáticamente por ASP.NET MVC Framework :)
(2) Una URL encuentra una coincidencia en la tabla de ruta, pero especifica un controlador inexistente.
(3) Una URL encuentra una coincidencia en la tabla de ruta, pero especifica una acción inexistente.
(Generado manualmente :)
(4) Una acción devuelve un HttpNotFoundResult utilizando el método HttpNotFound ().
(5) Una acción lanza una HttpException con el código de estado 404.
(6) Una acción modifica manualmente la propiedad Response.StatusCode a 404.
Normalmente, quieres cumplir 3 objetivos:
(1) Mostrar una página de error 404 personalizada para el usuario.
(2) Mantenga el código de estado 404 en la respuesta del cliente (especialmente importante para SEO).
(3) Envíe la respuesta directamente, sin implicar una redirección 302.
Hay varias maneras de tratar de lograr esto:
(1)
<system.web>
<customErrors mode="On">
<error statusCode="404" redirect="~/Errors/NotFound"/>
</customError>
</system.web>
Problemas con esta solución:
- No cumple con el objetivo (1) en los casos (1), (4), (6).
- No cumple con el objetivo (2) automáticamente. Debe ser programado manualmente.
- No cumple con el objetivo (3).
(2)
<system.webServer>
<httpErrors errorMode="Custom">
<remove statusCode="404"/>
<error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
</httpErrors>
</system.webServer>
Problemas con esta solución:
- Solo funciona en IIS 7+.
- No cumple con el objetivo (1) en los casos (2), (3), (5).
- No cumple con el objetivo (2) automáticamente. Debe ser programado manualmente.
(3)
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="404"/>
<error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
</httpErrors>
</system.webServer>
Problemas con esta solución:
- Solo funciona en IIS 7+.
- No cumple con el objetivo (2) automáticamente. Debe ser programado manualmente.
- Oculta las excepciones de http a nivel de aplicación. Por ejemplo, no se puede usar la sección customErrors, System.Web.Mvc.HandleErrorAttribute, etc. No solo puede mostrar páginas de error genéricas.
(4)
<system.web>
<customErrors mode="On">
<error statusCode="404" redirect="~/Errors/NotFound"/>
</customError>
</system.web>
y
<system.webServer>
<httpErrors errorMode="Custom">
<remove statusCode="404"/>
<error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
</httpErrors>
</system.webServer>
Problemas con esta solución:
- Solo funciona en IIS 7+.
- No cumple con el objetivo (2) automáticamente. Debe ser programado manualmente.
- No cumple con el objetivo (3) en los casos (2), (3), (5).
Las personas que han tenido problemas con esto antes incluso intentaron crear sus propias bibliotecas (consulte http://aboutcode.net/2011/02/26/handling-not-found-with-asp-net-mvc3.html ). Pero la solución anterior parece cubrir todos los casos sin la complejidad de usar una biblioteca externa.
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
1) Hacer clase abstracta de controlador.
public abstract class MyController:Controller
{
public ActionResult NotFound()
{
Response.StatusCode = 404;
return View("NotFound");
}
protected override void HandleUnknownAction(string actionName)
{
this.ActionInvoker.InvokeAction(this.ControllerContext, "NotFound");
}
protected override void OnAuthorization(AuthorizationContext filterContext) { }
}
2) Haga herencia de esta clase abstracta en todos sus controladores
public class HomeController : MyController
{}
3) Y agregue una vista llamada "NotFound" en su carpeta Vista compartida.
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
He revisado todos los artículos, pero nada funciona para mí: el usuario de mi requisito debe escribir cualquier cosa en su página 404 personalizada de url. Creo que es muy sencillo. Pero debe entender el manejo de 404 correctamente:
<system.web>
<customErrors mode="On" redirectMode="ResponseRewrite">
<error statusCode="404" redirect="~/PageNotFound.aspx"/>
</customErrors>
</system.web>
<system.webServer>
<httpErrors errorMode="Custom">
<remove statusCode="404"/>
<error statusCode="404" path="/PageNotFound.html" responseMode="ExecuteURL"/>
</httpErrors>
</system.webServer>
Encontré este artículo muy útil. Debería leerse al mismo tiempo. Custome error página-Ben Foster
Mi solución abreviada que funciona con áreas no controladas, controladores y acciones:
Crear una vista 404.cshtml.
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"); } }
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(); } }
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.cshtml
en la Shared
carpeta, y eso es todo.
Publicar una respuesta ya que mi comentario fue demasiado largo ...
Es tanto un comentario como una pregunta para la publicación / respuesta de unicornio:
https://.com/a/7499406/687549
Prefiero esta respuesta a las otras por su simplicidad y el hecho de que aparentemente se consultó a algunas personas de Microsoft. Sin embargo, tengo tres preguntas y si se pueden responder, llamaré a esta respuesta el santo grial de todas las respuestas de error 404/500 en las interwebs para una aplicación ASP.NET MVC (x).
@ Pure.Krome
¿Puede actualizar su respuesta con las cosas de SEO de los comentarios señalados por GWB (nunca hubo ninguna mención de esto en su respuesta) -
<customErrors mode="On" redirectMode="ResponseRewrite">
y<httpErrors errorMode="Custom" existingResponse="Replace">
?¿Puedes preguntarle a tus amigos del equipo de ASP.NET si está bien hacerlo así? Sería bueno tener alguna confirmación. ¡¿Tal vez sea un gran no-no cambiar
redirectMode
yexistingResponse
de esta manera poder jugar bien con SEO ?!Se puede añadir algunas aclaraciones que rodea todo eso (
customErrors redirectMode="ResponseRewrite"
,customErrors redirectMode="ResponseRedirect"
,httpErrors errorMode="Custom" existingResponse="Replace"
, retirecustomErrors
completamente como alguien sugirió) después de hablar con sus amigos en Microsoft?
Como decía; sería fantástico si pudiéramos hacer su respuesta más completa, ya que parece ser una pregunta bastante popular con más de 54 000 visitas.
Actualización : la respuesta de Unicornio hace un 302 Found y un 200 OK y no se puede cambiar para devolver solo 404 con una ruta. Tiene que ser un archivo físico que no es muy MVC: ish. Así que pasar a otra solución. Lástima porque este parece ser el MVC definitivo: responde tan lejos.
Me parece que la CustomErrors
configuración estándar debería funcionar sin embargo, debido a la confianza en Server.Transfer
ella, parece que la implementación interna de ResponseRewrite
no 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.TransferRequest
funciona. 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.
Pruebe NotFoundMVC en nuget. Funciona, no hay configuración.
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.