try practice handling exceptions exceptionfilterattribute error catch best c# asp.net-mvc-4 asp.net-web-api2 owin fluentvalidation

c# - practice - web api error handler



WebAPi: unifica el formato de los mensajes de error de ApiController y OAuthAuthorizationServerProvider (2)

En mi proyecto WebAPI estoy usando Owin.Security.OAuth para agregar autenticación JWT. Dentro de GrantResourceOwnerCredentials de mi OAuthProvider, estoy configurando errores usando la línea siguiente:

context.SetError("invalid_grant", "Account locked.");

esto se devuelve al cliente como:

{ "error": "invalid_grant", "error_description": "Account locked." }

después de que el usuario se autentica e intenta hacer una solicitud "normal" a uno de mis controladores, obtiene una respuesta inferior cuando el modelo no es válido (usando FluentValidation):

{ "message": "The request is invalid.", "modelState": { "client.Email": [ "Email is not valid." ], "client.Password": [ "Password is required." ] } }

Ambas solicitudes devuelven 400 Bad Request , pero a veces debe buscar el campo error_description y, a veces, el message

Pude crear un mensaje de respuesta personalizado, pero esto solo se aplica a los resultados que estoy devolviendo.

Mi pregunta es: ¿es posible reemplazar el message con error en la respuesta que devuelve ModelValidatorProviders y en otros lugares?

He leído sobre ExceptionFilterAttribute pero no sé si este es un buen lugar para comenzar. FluentValidation no debería ser un problema, porque todo lo que hace es agregar errores a ModelState .

EDITAR:
Lo siguiente que trato de corregir es una convención de nomenclatura incoherente en los datos devueltos en WebApi: cuando devuelvo el error de OAuthProvider tenemos error_details , pero al devolver BadRequest con ModelState (desde ApiController ) tenemos modelState . Como puede ver, primero usa snake_case y second camelCase .


¿Es posible reemplazar mensaje con error en la respuesta que devuelve ModelValidatorProviders

Podemos usar SetError sobrecargado para hacerlo; de lo contrario, reemplace el error con un mensaje.

BaseValidatingContext<TOptions>.SetError Method (String)

Marca este contexto como no validado por la aplicación y asigna varias propiedades de información de error. HasError se convierte en verdadero y IsValidated se convierte en falso como resultado de una llamada.

string msg = "{/"message/": /"Account locked./"}"; context.SetError(msg); Response.StatusCode = 400; context.Response.Write(msg);


RESPUESTA ACTUALIZADA (Usar Middleware)

Dado que la idea original del gestor de delegación de Web API significaba que no sería lo suficientemente temprano en la tubería como el middleware de OAuth, entonces se necesita crear un middleware personalizado ...

public static class ErrorMessageFormatter { public static IAppBuilder UseCommonErrorResponse(this IAppBuilder app) { app.Use<JsonErrorFormatter>(); return app; } public class JsonErrorFormatter : OwinMiddleware { public JsonErrorFormatter(OwinMiddleware next) : base(next) { } public override async Task Invoke(IOwinContext context) { var owinRequest = context.Request; var owinResponse = context.Response; //buffer the response stream for later var owinResponseStream = owinResponse.Body; //buffer the response stream in order to intercept downstream writes using (var responseBuffer = new MemoryStream()) { //assign the buffer to the resonse body owinResponse.Body = responseBuffer; await Next.Invoke(context); //reset body owinResponse.Body = owinResponseStream; if (responseBuffer.CanSeek && responseBuffer.Length > 0 && responseBuffer.Position > 0) { //reset buffer to read its content responseBuffer.Seek(0, SeekOrigin.Begin); } if (!IsSuccessStatusCode(owinResponse.StatusCode) && responseBuffer.Length > 0) { //NOTE: perform your own content negotiation if desired but for this, using JSON var body = await CreateCommonApiResponse(owinResponse, responseBuffer); var content = JsonConvert.SerializeObject(body); var mediaType = MediaTypeHeaderValue.Parse(owinResponse.ContentType); using (var customResponseBody = new StringContent(content, Encoding.UTF8, mediaType.MediaType)) { var customResponseStream = await customResponseBody.ReadAsStreamAsync(); await customResponseStream.CopyToAsync(owinResponseStream, (int)customResponseStream.Length, owinRequest.CallCancelled); owinResponse.ContentLength = customResponseStream.Length; } } else { //copy buffer to response stream this will push it down to client await responseBuffer.CopyToAsync(owinResponseStream, (int)responseBuffer.Length, owinRequest.CallCancelled); owinResponse.ContentLength = responseBuffer.Length; } } } async Task<object> CreateCommonApiResponse(IOwinResponse response, Stream stream) { var json = await new StreamReader(data).ReadToEndAsync(); var statusCode = ((HttpStatusCode)response.StatusCode).ToString(); var responseReason = response.ReasonPhrase ?? statusCode; //Is this a HttpError var httpError = JsonConvert.DeserializeObject<HttpError>(json); if (httpError != null) { return new { error = httpError.Message ?? responseReason, error_description = (object)httpError.MessageDetail ?? (object)httpError.ModelState ?? (object)httpError.ExceptionMessage }; } //Is this an OAuth Error var oAuthError = Newtonsoft.Json.Linq.JObject.Parse(json); if (oAuthError["error"] != null && oAuthError["error_description"] != null) { dynamic obj = oAuthError; return new { error = (string)obj.error, error_description = (object)obj.error_description }; } //Is this some other unknown error (Just wrap in common model) var error = JsonConvert.DeserializeObject(json); return new { error = responseReason, error_description = error }; } bool IsSuccessStatusCode(int statusCode) { return statusCode >= 200 && statusCode <= 299; } } }

... y se registró al principio de la canalización antes de que se agreguen los middlewares de autenticación y los controladores de API web.

public class Startup { public void Configuration(IAppBuilder app) { app.UseResponseEncrypterMiddleware(); app.UseRequestLogger(); //...(after logging middle ware) app.UseCommonErrorResponse(); //... (before auth middle ware) //...code removed for brevity } }

Este ejemplo es solo un comienzo básico. Debería ser lo suficientemente simple como para extender este punto de partida.

Aunque en este ejemplo el modelo común se parece a lo que devuelve OAuthProvider, se puede usar cualquier modelo de objeto común.

Lo probé con unas pocas pruebas de unidades en la memoria y, a través de TDD, pude hacerlo funcionar.

[TestClass] public class UnifiedErrorMessageTests { [TestMethod] public async Task _OWIN_Response_Should_Pass_When_Ok() { //Arrange var message = "/"Hello World/""; var expectedResponse = "/"I am working/""; using (var server = TestServer.Create<WebApiTestStartup>()) { var client = server.HttpClient; client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var content = new StringContent(message, Encoding.UTF8, "application/json"); //Act var response = await client.PostAsync("/api/Foo", content); //Assert Assert.IsTrue(response.IsSuccessStatusCode); var result = await response.Content.ReadAsStringAsync(); Assert.AreEqual(expectedResponse, result); } } [TestMethod] public async Task _OWIN_Response_Should_Be_Unified_When_BadRequest() { //Arrange var expectedResponse = "invalid_grant"; using (var server = TestServer.Create<WebApiTestStartup>()) { var client = server.HttpClient; client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var content = new StringContent(expectedResponse, Encoding.UTF8, "application/json"); //Act var response = await client.PostAsync("/api/Foo", content); //Assert Assert.IsFalse(response.IsSuccessStatusCode); var result = await response.Content.ReadAsAsync<dynamic>(); Assert.AreEqual(expectedResponse, (string)result.error_description); } } [TestMethod] public async Task _OWIN_Response_Should_Be_Unified_When_MethodNotAllowed() { //Arrange var expectedResponse = "Method Not Allowed"; using (var server = TestServer.Create<WebApiTestStartup>()) { var client = server.HttpClient; client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); //Act var response = await client.GetAsync("/api/Foo"); //Assert Assert.IsFalse(response.IsSuccessStatusCode); var result = await response.Content.ReadAsAsync<dynamic>(); Assert.AreEqual(expectedResponse, (string)result.error); } } [TestMethod] public async Task _OWIN_Response_Should_Be_Unified_When_NotFound() { //Arrange var expectedResponse = "Not Found"; using (var server = TestServer.Create<WebApiTestStartup>()) { var client = server.HttpClient; client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); //Act var response = await client.GetAsync("/api/Bar"); //Assert Assert.IsFalse(response.IsSuccessStatusCode); var result = await response.Content.ReadAsAsync<dynamic>(); Assert.AreEqual(expectedResponse, (string)result.error); } } public class WebApiTestStartup { public void Configuration(IAppBuilder app) { app.UseCommonErrorMessageMiddleware(); var config = new HttpConfiguration(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); app.UseWebApi(config); } } public class FooController : ApiController { public FooController() { } [HttpPost] public IHttpActionResult Bar([FromBody]string input) { if (input == "Hello World") return Ok("I am working"); return BadRequest("invalid_grant"); } } }

RESPUESTA ORIGINAL (Use DelegatingHandler)

Considere usar un DelegatingHandler

Citando de un artículo encontrado en línea.

Los manipuladores delegados son extremadamente útiles para problemas transversales. Se conectan en las etapas iniciales y finales de la cartera de solicitudes y respuestas, lo que los hace ideales para manipular la respuesta justo antes de enviarla al cliente.

Este ejemplo es un intento simplificado del mensaje de error unificado para respuestas HttpError

public class HttpErrorHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); return NormalizeResponse(request, response); } private HttpResponseMessage NormalizeResponse(HttpRequestMessage request, HttpResponseMessage response) { object content; if (!response.IsSuccessStatusCode && response.TryGetContentValue(out content)) { var error = content as HttpError; if (error != null) { var unifiedModel = new { error = error.Message, error_description = (object)error.MessageDetail ?? error.ModelState }; var newResponse = request.CreateResponse(response.StatusCode, unifiedModel); foreach (var header in response.Headers) { newResponse.Headers.Add(header.Key, header.Value); } return newResponse; } } return response; } }

Aunque este ejemplo es muy básico, es trivial ampliarlo para adaptarlo a sus necesidades personalizadas.

Ahora solo se trata de agregar el controlador a la tubería

public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MessageHandlers.Add(new HttpErrorHandler()); // Other code not shown... } }

Los manejadores de mensajes se invocan en el mismo orden en que aparecen en la colección MessageHandlers . Debido a que están anidados, el mensaje de respuesta viaja en la otra dirección. Es decir, el último controlador es el primero en recibir el mensaje de respuesta.

Fuente: manejadores de mensajes HTTP en ASP.NET Web API