ASP.NET MVC Custom Error Handling Application_Error Global.asax?

108

Eu tenho alguns códigos básicos para determinar erros em meu aplicativo MVC. Actualmente no meu projeto eu tenho um controlador chamado Errorcom métodos de ação HTTPError404(), HTTPError500()e General(). Todos eles aceitam um parâmetro de string error. Usando ou modificando o código abaixo. Qual é a melhor / adequada maneira de passar os dados para o controlador de erro para processamento? Gostaria de ter uma solução o mais robusta possível.

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
fonte

Respostas:

104

Em vez de criar uma nova rota para isso, você pode apenas redirecionar para seu controlador / ação e passar as informações por meio de querystring. Por exemplo:

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

Em seguida, seu controlador receberá o que você quiser:

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

Existem algumas desvantagens em sua abordagem. Tenha muito cuidado com o loop neste tipo de tratamento de erros. Outra coisa é que, como você está percorrendo o pipeline do asp.net para lidar com um 404, criará um objeto de sessão para todos esses acessos. Isso pode ser um problema (desempenho) para sistemas muito usados.

andrecarlucci
fonte
Quando você diz "tenha cuidado com o loop", o que exatamente você quer dizer? Existe uma maneira melhor de lidar com esse tipo de redirecionamento de erro (presumindo que seja um sistema muito usado)?
aherrick
4
Por looping, quero dizer que quando você tem um erro em sua página de erro, então você será redirecionado para sua página de erro novamente ... (por exemplo, você deseja registrar seu erro em um banco de dados e ele está fora do ar).
andrecarlucci de
125
O redirecionamento em caso de erros vai contra a arquitetura da web. O URI deve permanecer o mesmo quando o servidor responde o código de status HTTP correto para que o cliente saiba o contexto exato da falha. Implementar HandleErrorAttribute.OnException ou Controller.OnException é uma solução melhor. E se isso falhar, faça um Server.Transfer ("~ / Error") em Global.asax.
Asbjørn Ulsberg
1
@Chris, é aceitável, mas não é a prática recomendada. Principalmente porque ele costuma ser redirecionado para um arquivo de recurso que é servido com um código de status HTTP 200, o que faz com que o cliente acredite que tudo correu bem.
Asbjørn Ulsberg
1
Tive que adicionar <httpErrors errorMode = "Detailed" /> ao web.config para fazer isso funcionar no servidor.
Jeroen K
28

Para responder à pergunta inicial "como passar corretamente os dados de roteamento para o controlador de erro?":

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

Em seguida, em sua classe ErrorController, implemente uma função como esta:

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

Isso empurra a exceção para a View. A página de visualização deve ser declarada da seguinte forma:

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

E o código para exibir o erro:

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

Esta é a função que reúne todas as mensagens de exceção da árvore de exceções:

    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
fonte
9

Eu encontrei uma solução para o problema de ajax observado 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ý
fonte
Ao implementar isso, recebi o seguinte erro: 'System.Web.HttpRequest' não contém uma definição para 'IsAjaxRequest'. Este artigo tem uma solução: stackoverflow.com/questions/14629304/…
Julian Dormon
8

Eu já lutava com a ideia de centralizar uma rotina global de tratamento de erros em um aplicativo MVC antes. Eu tenho uma postagem nos fóruns ASP.NET .

Basicamente, ele lida com todos os erros do seu aplicativo no global.asax sem a necessidade de um controlador de erro, decorando com o [HandlerError]atributo ou mexendo no customErrorsnó do web.config.

Jack Hsu
fonte
6

Talvez a melhor maneira de lidar com erros no MVC seja aplicar o atributo HandleError ao seu controlador ou ação e atualizar o arquivo Shared / Error.aspx para fazer o que você deseja. O objeto Model nessa página inclui uma propriedade Exception, bem como ControllerName e ActionName.

Brian
fonte
1
Como você lidará com um 404erro então? já que não há um controlador / ação designado para isso?
Demência de
A resposta aceita inclui 404s. Essa abordagem só é útil para 500 erros.
Brian
Talvez você deva editar isso em sua resposta. Perhaps a better way of handling errorsparece muito com Todos os erros e não apenas 500.
Demência de
4

Application_Error tendo problemas com solicitações Ajax. Se o erro for tratado no Action que é chamado pelo Ajax - ele exibirá sua Error View dentro do container resultante.

Victor Gelmutdinov
fonte
4

Esta pode não ser a melhor maneira para MVC ( https://stackoverflow.com/a/9461386/5869805 )

Abaixo está como você renderiza uma visão em Application_Error e escreve para uma resposta http. Você não precisa usar o redirecionamento. Isso evitará uma segunda solicitação ao servidor, de modo que o link na barra de endereços do navegador permanecerá o mesmo. Isso pode ser bom ou ruim, depende do que você deseja.

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.
}

Visão

@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
fonte
Esta é a melhor forma IMO. Exatamente o que eu estava procurando.
Steve Harris de
@SteveHarris que bom que ajudou! :)
burkay
3

Brian, Esta abordagem funciona muito bem para solicitações não Ajax, mas como Lion_cl afirmou, se você tiver um erro durante uma chamada de Ajax, sua visão Share / Error.aspx (ou sua visão de página de erro personalizada) será retornada ao chamador Ajax- -o usuário NÃO será redirecionado para a página de erro.

inegavelmente rob
fonte
0

Use o seguinte código para redirecionar na página de rota. Use exception.Message instide of exception. Coz exceção string de consulta dá erro se estende o comprimento da querystring.

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

Tenho problemas com esta abordagem de tratamento de erros: No caso de web.config:

<customErrors mode="On"/>

O manipulador de erros está pesquisando a visualização Error.shtml e a etapa do fluxo de controle em Application_Error global.asax somente após a exceção

System.InvalidOperationException: A visualização 'Erro' ou seu mestre não foi encontrado ou nenhum mecanismo de visualização oferece suporte aos locais pesquisados. Os seguintes locais foram pesquisados: ~ / Views / home / Error.aspx ~ / Views / home / Error.ascx ~ / Views / Shared / Error.aspx ~ / Views / Shared / Error.ascx ~ / Views / home / Error. cshtml ~ / Views / home / Error.vbhtml ~ / Views / Shared / Error.cshtml ~ / Views / Shared / Error.vbhtml em System.Web.Mvc.ViewResult.FindView (contexto ControllerContext) ........ ............

assim

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

httpException is always null then customErrors mode = "On" :( É enganoso Então <customErrors mode="Off"/>ou <customErrors mode="RemoteOnly"/>os usuários veem customErrors html, Then customErrors mode = "On" este código também está errado


Outro problema deste código que

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

Retorne a página com o código 302 em vez do código de erro real (402.403 etc)

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