ASP.NET MVC Manejo de errores personalizado Application_Error Global.asax?

108

Tengo un código básico para determinar errores en mi aplicación MVC. Actualmente en mi proyecto tengo un controlador llamado Errorcon métodos de acción HTTPError404(), HTTPError500()y General(). Todos aceptan un parámetro de cadena error. Usando o modificando el código a continuación. ¿Cuál es la forma mejor / adecuada de pasar los datos al controlador de errores para su procesamiento? Me gustaría tener una solución lo más sólida posible.

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    Response.Clear();

    HttpException httpException = exception as HttpException;
    if (httpException != null)
    {
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "Error");
        switch (httpException.GetHttpCode())
        {
            case 404:
                // page not found
                routeData.Values.Add("action", "HttpError404");
                break;
            case 500:
                // server error
                routeData.Values.Add("action", "HttpError500");
                break;
            default:
                routeData.Values.Add("action", "General");
                break;
        }
        routeData.Values.Add("error", exception);
        // clear error on server
        Server.ClearError();

        // at this point how to properly pass route data to error controller?
    }
}
aherrick
fuente

Respuestas:

104

En lugar de crear una nueva ruta para eso, puede simplemente redirigir a su controlador / acción y pasar la información a través de la cadena de consulta. Por ejemplo:

protected void Application_Error(object sender, EventArgs e) {
  Exception exception = Server.GetLastError();
  Response.Clear();

  HttpException httpException = exception as HttpException;

  if (httpException != null) {
    string action;

    switch (httpException.GetHttpCode()) {
      case 404:
        // page not found
        action = "HttpError404";
        break;
      case 500:
        // server error
        action = "HttpError500";
        break;
      default:
        action = "General";
        break;
      }

      // clear error on server
      Server.ClearError();

      Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));
    }

Entonces tu controlador recibirá lo que quieras:

// GET: /Error/HttpError404
public ActionResult HttpError404(string message) {
   return View("SomeView", message);
}

Hay algunas compensaciones con su enfoque. Tenga mucho cuidado con los bucles en este tipo de manejo de errores. Otra cosa es que, dado que está pasando por el canal de asp.net para manejar un 404, creará un objeto de sesión para todos esos hits. Esto puede ser un problema (rendimiento) para sistemas muy utilizados.

andrecarlucci
fuente
Cuando dices "ten cuidado con los bucles", ¿qué quieres decir exactamente? ¿Existe una mejor manera de manejar este tipo de redireccionamiento de errores (asumiendo que FUE un sistema muy usado)?
aherrick
4
Con bucle me refiero a que cuando tenga un error en su página de error, entonces será redirigido a su página de error una y otra vez ... (por ejemplo, desea registrar su error en una base de datos y no funciona).
andrecarlucci
125
Redirigir los errores va en contra de la arquitectura de la web. El URI debe permanecer igual cuando el servidor responde con el código de estado HTTP correcto para que el cliente conozca el contexto exacto del error. Implementar HandleErrorAttribute.OnException o Controller.OnException es una mejor solución. Y si fallan, haga un Server.Transfer ("~ / Error") en Global.asax.
Asbjørn Ulsberg
1
@Chris, es aceptable, pero no es la mejor práctica. Especialmente porque a menudo se redirige a un archivo de recursos que se sirve con un código de estado HTTP 200, lo que hace que el cliente crea que todo salió bien.
Asbjørn Ulsberg
1
Tuve que agregar <httpErrors errorMode = "Detail" /> al web.config para que esto funcione en el servidor.
Jeroen K
28

Para responder a la pregunta inicial "¿cómo pasar correctamente los datos de ruta al controlador de errores?":

IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

Luego, en su clase ErrorController, implemente una función como esta:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Error(Exception exception)
{
    return View("Error", exception);
}

Esto empuja la excepción a la Vista. La página de visualización debe declararse de la siguiente manera:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<System.Exception>" %>

Y el código para mostrar el error:

<% if(Model != null) { %>  <p><b>Detailed error:</b><br />  <span class="error"><%= Helpers.General.GetErrorMessage((Exception)Model, false) %></span></p> <% } %>

Aquí está la función que recopila todos los mensajes de excepción del árbol de excepciones:

    public static string GetErrorMessage(Exception ex, bool includeStackTrace)
    {
        StringBuilder msg = new StringBuilder();
        BuildErrorMessage(ex, ref msg);
        if (includeStackTrace)
        {
            msg.Append("\n");
            msg.Append(ex.StackTrace);
        }
        return msg.ToString();
    }

    private static void BuildErrorMessage(Exception ex, ref StringBuilder msg)
    {
        if (ex != null)
        {
            msg.Append(ex.Message);
            msg.Append("\n");
            if (ex.InnerException != null)
            {
                BuildErrorMessage(ex.InnerException, ref msg);
            }
        }
    }
Tim Cooper
fuente
9

Encontré una solución para el problema de ajax señalado por Lion_cl.

global.asax:

protected void Application_Error()
    {           
        if (HttpContext.Current.Request.IsAjaxRequest())
        {
            HttpContext ctx = HttpContext.Current;
            ctx.Response.Clear();
            RequestContext rc = ((MvcHandler)ctx.CurrentHandler).RequestContext;
            rc.RouteData.Values["action"] = "AjaxGlobalError";

            // TODO: distinguish between 404 and other errors if needed
            rc.RouteData.Values["newActionName"] = "WrongRequest";

            rc.RouteData.Values["controller"] = "ErrorPages";
            IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
            IController controller = factory.CreateController(rc, "ErrorPages");
            controller.Execute(rc);
            ctx.Server.ClearError();
        }
    }

ErrorPagesController

public ActionResult AjaxGlobalError(string newActionName)
    {
        return new AjaxRedirectResult(Url.Action(newActionName), this.ControllerContext);
    }

AjaxRedirectResult

public class AjaxRedirectResult : RedirectResult
{
    public AjaxRedirectResult(string url, ControllerContext controllerContext)
        : base(url)
    {
        ExecuteResult(controllerContext);
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            JavaScriptResult result = new JavaScriptResult()
            {
                Script = "try{history.pushState(null,null,window.location.href);}catch(err){}window.location.replace('" + UrlHelper.GenerateContentUrl(this.Url, context.HttpContext) + "');"
            };

            result.ExecuteResult(context);
        }
        else
        {
            base.ExecuteResult(context);
        }
    }
}

AjaxRequestExtension

public static class AjaxRequestExtension
{
    public static bool IsAjaxRequest(this HttpRequest request)
    {
        return (request.Headers["X-Requested-With"] != null && request.Headers["X-Requested-With"] == "XMLHttpRequest");
    }
}
Jozef Krchňavý
fuente
Mientras implementaba esto, recibí el siguiente error: 'System.Web.HttpRequest' no contiene una definición para 'IsAjaxRequest'. Este artículo tiene una solución: stackoverflow.com/questions/14629304/…
Julian Dormon
8

Luché con la idea de centralizar una rutina de manejo de errores global en una aplicación MVC antes. Tengo una publicación en los foros de ASP.NET .

Básicamente, maneja todos los errores de su aplicación en global.asax sin la necesidad de un controlador de errores, decorar con el [HandlerError]atributo o jugar con el customErrorsnodo en web.config.

Jack Hsu
fuente
6

Quizás una mejor forma de manejar los errores en MVC es aplicar el atributo HandleError a su controlador o acción y actualizar el archivo Shared / Error.aspx para hacer lo que desee. El objeto Model de esa página incluye una propiedad Exception, así como ControllerName y ActionName.

Brian
fuente
1
404Entonces, ¿ cómo manejará un error? ya que no hay ningún controlador / acción designado para eso?
Dementic
La respuesta aceptada incluye 404. Este enfoque solo es útil para 500 errores.
Brian
Quizás deberías editar eso en tu respuesta. Perhaps a better way of handling errorssuena más o menos como Todos los errores y no solo 500.
Dementic
4

Application_Error tiene problemas con las solicitudes de Ajax. Si el error se manejó en la Acción que llamó Ajax, se mostrará su Vista de error dentro del contenedor resultante.

Victor Gelmutdinov
fuente
4

Esta puede no ser la mejor manera para MVC ( https://stackoverflow.com/a/9461386/5869805 )

A continuación se muestra cómo renderiza una vista en Application_Error y la escribe en la respuesta http. No es necesario utilizar la redirección. Esto evitará una segunda solicitud al servidor, por lo que el enlace en la barra de direcciones del navegador permanecerá igual. Esto puede ser bueno o malo, depende de lo que quieras.

Global.asax.cs

protected void Application_Error()
{
    var exception = Server.GetLastError();
    // TODO do whatever you want with exception, such as logging, set errorMessage, etc.
    var errorMessage = "SOME FRIENDLY MESSAGE";

    // TODO: UPDATE BELOW FOUR PARAMETERS ACCORDING TO YOUR ERROR HANDLING ACTION
    var errorArea = "AREA";
    var errorController = "CONTROLLER";
    var errorAction = "ACTION";
    var pathToViewFile = $"~/Areas/{errorArea}/Views/{errorController}/{errorAction}.cshtml"; // THIS SHOULD BE THE PATH IN FILESYSTEM RELATIVE TO WHERE YOUR CSPROJ FILE IS!

    var requestControllerName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["controller"]);
    var requestActionName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["action"]);

    var controller = new BaseController(); // REPLACE THIS WITH YOUR BASE CONTROLLER CLASS
    var routeData = new RouteData { DataTokens = { { "area", errorArea } }, Values = { { "controller", errorController }, {"action", errorAction} } };
    var controllerContext = new ControllerContext(new HttpContextWrapper(HttpContext.Current), routeData, controller);
    controller.ControllerContext = controllerContext;

    var sw = new StringWriter();
    var razorView = new RazorView(controller.ControllerContext, pathToViewFile, "", false, null);
    var model = new ViewDataDictionary(new HandleErrorInfo(exception, requestControllerName, requestActionName));
    var viewContext = new ViewContext(controller.ControllerContext, razorView, model, new TempDataDictionary(), sw);
    viewContext.ViewBag.ErrorMessage = errorMessage;
    //TODO: add to ViewBag what you need
    razorView.Render(viewContext, sw);
    HttpContext.Current.Response.Write(sw);
    Server.ClearError();
    HttpContext.Current.Response.End(); // No more processing needed (ex: by default controller/action routing), flush the response out and raise EndRequest event.
}

Ver

@model HandleErrorInfo
@{
    ViewBag.Title = "Error";
    // TODO: SET YOUR LAYOUT
}
<div class="">
    ViewBag.ErrorMessage
</div>
@if(Model != null && HttpContext.Current.IsDebuggingEnabled)
{
    <div class="" style="background:khaki">
        <p>
            <b>Exception:</b> @Model.Exception.Message <br/>
            <b>Controller:</b> @Model.ControllerName <br/>
            <b>Action:</b> @Model.ActionName <br/>
        </p>
        <div>
            <pre>
                @Model.Exception.StackTrace
            </pre>
        </div>
    </div>
}
burkay
fuente
Esta es la mejor manera en mi opinión. Exactamente lo que estaba buscando.
Steve Harris
@SteveHarris ¡me alegro de haber ayudado! :)
burkay
3

Brian, este enfoque funciona muy bien para solicitudes que no son de Ajax, pero como dijo Lion_cl, si tiene un error durante una llamada de Ajax, su vista Share / Error.aspx (o su vista de página de error personalizada) se devolverá al llamador de Ajax. -El usuario NO será redirigido a la página de error.

innegablemente robar
fuente
0

Utilice el siguiente código para redireccionar en la página de ruta. Usar excepción Mensaje a instancias de excepción. La cadena de consulta de excepción de Coz da error si extiende la longitud de la cadena de consulta.

routeData.Values.Add("error", exception.Message);
// clear error on server
Server.ClearError();
Response.RedirectToRoute(routeData.Values);
Swapnil Malap
fuente
-1

Tengo un problema con este enfoque de manejo de errores: en el caso de web.config:

<customErrors mode="On"/>

El controlador de errores está buscando en la vista Error.shtml y el paso de flujo de control en Application_Error global.asax solo después de la excepción

System.InvalidOperationException: No se encontró la vista 'Error' o su maestro o ningún motor de vista admite las ubicaciones buscadas. Se buscaron las siguientes ubicaciones: ~ / Vistas / inicio / Error.aspx ~ / Vistas / inicio / Error.ascx ~ / Vistas / Compartido / Error.aspx ~ / Vistas / Compartido / Error.ascx ~ / Vistas / inicio / Error. cshtml ~ / Views / home / Error.vbhtml ~ / Views / Shared / Error.cshtml ~ / Views / Shared / Error.vbhtml en System.Web.Mvc.ViewResult.FindView (contexto ControllerContext) ........ ............

Entonces

 Exception exception = Server.GetLastError();
  Response.Clear();
  HttpException httpException = exception as HttpException;

httpException es siempre nulo, luego customErrors mode = "On" :( Es engañoso Entonces <customErrors mode="Off"/>o <customErrors mode="RemoteOnly"/>los usuarios ven customErrors html, Entonces customErrors mode = "On" este código también es incorrecto


Otro problema de este código que

Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));

Devuelve la página con el código 302 en lugar del código de error real (402,403, etc.)

Александр Шмыков
fuente