Manejo global de excepciones en middleware OWIN


Estoy tratando de crear un manejo unificado de errores / informes en ASP.NET Web API 2.1 Proyecto construido sobre middleware OWIN (HOST IIS usando Owin.Host.SystemWeb). Actualmente usé un registrador de excepciones personalizado que hereda de System.Web.Http.ExceptionHandling.ExceptionLogger y usa NLog para registrar todas las excepciones como el siguiente código:

public class NLogExceptionLogger : ExceptionLogger
{

    private static readonly Logger Nlog = LogManager.GetCurrentClassLogger();
    public override void Log(ExceptionLoggerContext context)
    {
       //Log using NLog
    } 
}

El quiero cambiar el cuerpo de la respuesta para todas las excepciones de la API a una respuesta unificada amigable que oculta todos los detalles de la excepción usando System.Web.Http.ExceptionHandling.ExceptionHandler como el código a continuación:

public class ContentNegotiatedExceptionHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        var errorDataModel = new ErrorDataModel
        {
            Message = "Internal server error occurred, error has been reported!",
            Details = context.Exception.Message,
            ErrorReference = context.Exception.Data["ErrorReference"] != null ? context.Exception.Data["ErrorReference"].ToString() : string.Empty,
            DateTime = DateTime.UtcNow
        };

        var response = context.Request.CreateResponse(HttpStatusCode.InternalServerError, errorDataModel);
        context.Result = new ResponseMessageResult(response);
    }
}

Y esto devolverá la respuesta siguiente para el cliente cuando ocurra una excepción:

{
  "Message": "Internal server error occurred, error has been reported!",
  "Details": "Ooops!",
  "ErrorReference": "56627a45d23732d2",
  "DateTime": "2015-12-27T09:42:40.2982314Z"
}

Ahora esto funciona muy bien si se produce alguna excepción w en una canalización de solicitud de controlador Api.

Pero en mi situación estoy usando el middleware Microsoft.Owin.Security.OAuth para generar tokens de portador, y este middleware no sabe nada sobre el manejo de excepciones de la API Web, por ejemplo, si se ha lanzado una excepción en el método ValidateClientAuthentication mi NLogExceptionLogger no ContentNegotiatedExceptionHandler sabrá nada sobre esta excepción ni tratar de manejarlo, el código de ejemplo que utilicé en el AuthorizationServerProvider es el siguiente:

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        //Expcetion occurred here
        int x = int.Parse("");

        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        if (context.UserName != context.Password)
        {
            context.SetError("invalid_credentials", "The user name or password is incorrect.");
            return;
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);

        identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));

        context.Validated(identity);
    }
}

Así que apreciaré cualquier orientación en la implementación de las siguientes 2 cuestiones:

1 - Crear un manejador de excepciones global que maneja solo las excepciones generadas por OWIN middle wares? Seguí esta respuesta y creé un middleware para fines de manejo de excepciones y lo registré como el primero y pude hacer excepciones de registro originadas desde "OAuthAuthorizationServerProvider", pero no estoy seguro de si esta es la forma óptima de hacerlo.

2 - Ahora, cuando implementé el registro como el en el paso anterior, realmente no tengo idea de cómo cambiar la respuesta de la excepción, ya que necesito devolver al cliente un modelo JSON estándar para cualquier excepción que ocurra en el "OAuthAuthorizationServerProvider". Hay una respuesta relacionada aquí En la que traté de depender, pero no funcionó.

Aquí está mi clase de inicio y el custom GlobalExceptionMiddleware Creé para la captura/registro de excepciones. La paz perdida está devolviendo una respuesta JSON unificada para cualquier excepción. Cualquier idea será apreciada.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var httpConfig = new HttpConfiguration();

        httpConfig.MapHttpAttributeRoutes();

        httpConfig.Services.Replace(typeof(IExceptionHandler), new ContentNegotiatedExceptionHandler());

        httpConfig.Services.Add(typeof(IExceptionLogger), new NLogExceptionLogger());

        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new AuthorizationServerProvider()
        };

        app.Use<GlobalExceptionMiddleware>();

        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        app.UseWebApi(httpConfig);
    }
}

public class GlobalExceptionMiddleware : OwinMiddleware
{
    public GlobalExceptionMiddleware(OwinMiddleware next)
        : base(next)
    { }

    public override async Task Invoke(IOwinContext context)
    {
        try
        {
            await Next.Invoke(context);
        }
        catch (Exception ex)
        {
            NLogLogger.LogError(ex, context);
        }
    }
}
Author: Community, 2015-12-27

2 answers

Ok, así que esto fue más fácil de lo previsto, gracias por @Khalid por el heads up, he terminado creando un middleware de owin llamado OwinExceptionHandlerMiddleware que está dedicado para manejar cualquier excepción que ocurra en cualquier Middleware de Owin (registrándolo y manipulando la respuesta antes de devolverlo al cliente).

Necesita registrar este middleware como el primero {[9] } en la clase Startup como el siguiente:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var httpConfig = new HttpConfiguration();

        httpConfig.MapHttpAttributeRoutes();

        httpConfig.Services.Replace(typeof(IExceptionHandler), new ContentNegotiatedExceptionHandler());

        httpConfig.Services.Add(typeof(IExceptionLogger), new NLogExceptionLogger());

        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new AuthorizationServerProvider()
        };

        //Should be the first handler to handle any exception happening in OWIN middlewares
        app.UseOwinExceptionHandler();

        // Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);

        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        app.UseWebApi(httpConfig);
    }
}

Y el código utilizado en el OwinExceptionHandlerMiddleware como el siguiente:

using AppFunc = Func<IDictionary<string, object>, Task>;

public class OwinExceptionHandlerMiddleware
{
    private readonly AppFunc _next;

    public OwinExceptionHandlerMiddleware(AppFunc next)
    {
        if (next == null)
        {
            throw new ArgumentNullException("next");
        }

        _next = next;
    }

    public async Task Invoke(IDictionary<string, object> environment)
    {
        try
        {
            await _next(environment);
        }
        catch (Exception ex)
        {
            try
            {

                var owinContext = new OwinContext(environment);

                NLogLogger.LogError(ex, owinContext);

                HandleException(ex, owinContext);

                return;
            }
            catch (Exception)
            {
                // If there's a Exception while generating the error page, re-throw the original exception.
            }
            throw;
        }
    }
    private void HandleException(Exception ex, IOwinContext context)
    {
        var request = context.Request;

        //Build a model to represet the error for the client
        var errorDataModel = NLogLogger.BuildErrorDataModel(ex);

        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ReasonPhrase = "Internal Server Error";
        context.Response.ContentType = "application/json";
        context.Response.Write(JsonConvert.SerializeObject(errorDataModel));

    }

}

public static class OwinExceptionHandlerMiddlewareAppBuilderExtensions
{
    public static void UseOwinExceptionHandler(this IAppBuilder app)
    {
        app.Use<OwinExceptionHandlerMiddleware>();
    }
}
 29
Author: Taiseer Joudeh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-29 21:14:37

Hay algunas maneras de hacer lo que quieres:

  1. Cree el middleware que está registrado primero , luego todas las excepciones se expandirán hasta ese middleware. En este punto, simplemente escriba su JSON a través del objeto Response a través del contexto OWIN.

  2. También puede crear un middleware de envoltura que envuelve el middleware de Oauth. En este caso lo hará en los errores de captura que se originan en esta ruta de código específica.

En última instancia, escribir su JSON el mensaje se trata de crearlo, serializarlo y escribirlo en la Respuesta a través del contexto OWIN.

Parece que estás en el camino correcto con #1. Espero que esto ayude, y buena suerte:)

 7
Author: Khalid Abuhakmeh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-27 15:42:24