WebAPi – 统一来自ApiController和OAuthAuthorizationServerProvider的错误消息格式

在我的WebAPI项目中,我使用Owin.Security.OAuth添加JWT身份validation。 在我的OAuthProvider的GrantResourceOwnerCredentials ,我使用下面的行设置错误:

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

这将返回给客户:

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

在用户获得身份validation并且他尝试向我的某个控制器执行“正常”请求后,当模型无效时,他会得到以下响应(使用FluentValidation):

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

这两个请求都返回400 Bad Request ,但有时您必须查找error_description字段,有时还要查找message

我能够创建自定义响应消息,但这仅适用于我返回的结果。

我的问题是:是否有可能在ModelValidatorProviders和其他地方返回的响应中替换error message

我已经阅读了ExceptionFilterAttribute但我不知道这是否是一个好的起点。 FluentValidation应该不是问题,因为它只是向ModelState添加错误。

编辑:
接下来我要修复的是WebApi中返回数据的命名约定不一致 – 当从OAuthProvider返回错误时我们有error_details ,但是当使用ModelState (来自ApiController )返回BadRequest ,我们有了modelState 。 你可以看到首先使用snake_case和第二个camelCase

更新的答案(使用中间件)

由于Web API原始委托处理程序的想法意味着它不像OAuth中间件那样在管道中足够早,因此需要创建自定义中间件……

 public static class ErrorMessageFormatter { public static IAppBuilder UseCommonErrorResponse(this IAppBuilder app) { app.Use(); 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 CreateCommonApiResponse(IOwinResponse response, Stream stream) { var json = await new StreamReader(stream).ReadToEndAsync(); var statusCode = ((HttpStatusCode)response.StatusCode).ToString(); var responseReason = response.ReasonPhrase ?? statusCode; //Is this a HttpError var httpError = JsonConvert.DeserializeObject(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; } } } 

...并在添加身份validation中间件和Web api处理程序之前在管道中注册。

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

这个例子只是一个基本的开始。 它应该足够简单,能够扩展这个起点。

虽然在此示例中,通用模型看起来像从OAuthProvider返回的内容,但可以使用任何常见的对象模型。

通过一些内存unit testing对其进行测试,并通过TDD能够使其正常工作。

 [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()) { 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()) { 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(); 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()) { 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(); 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()) { 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(); 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"); } } } 

原始答案(使用DelegatingHandler)

考虑使用DelegatingHandler

引自在线发现的文章。

委派处理程序对于交叉问题非常有用。 它们挂钩到请求 - 响应管道的非常早期和非常晚的阶段,使它们成为在将响应发送回客户端之前进行操作的理想选择。

此示例是针对HttpError响应的统一错误消息的简化尝试

 public class HttpErrorHandler : DelegatingHandler { protected override async Task 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; } } 

虽然这个示例非常基础,但扩展它以满足您的自定义需求是微不足道的。

现在只需将处理程序添加到管道即可

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

消息处理程序的调用顺序与它们在MessageHandlers集合中的显示顺序相同。 因为它们是嵌套的,所以响应消息以另一个方向传播。 也就是说,最后一个处理程序是第一个获取响应消息的处理程序。

源: ASP.NET Web API中的HTTP消息处理程序

是否可以在ModelValidatorProviders返回的响应中替换带有错误的消息

我们可能会使用重载的SetError来执行此操作,将错误替换为message。

 BaseValidatingContext.SetError Method (String) 

将此上下文标记为未由应用程序validation,并分配各种错误信息属性。 HasError变为true,并且IsValidated因调用而变为false。

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