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
...并在添加身份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);
- ASP.NET MVC 4,EF5,模型中的独特属性 – 最佳实践?
- 如何使用Razor语法获取ASP.NET MVC 4中文本中URL的链接?
- ResolveBundleUrl无法解析所有文件?
- 如何在MVC 3中将WebForms .ascx显示为局部视图
- 在MVC类上创建主键字段
- 下拉列表更好地作为ViewBag或模型C#/ .NET MVC4的一部分
- 从MVC WebApi调用时挂起Git / SSH
- Azure WebSites或Azure云服务上的MVC4 API :’System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption’
- 如何显示打开/保存对话框asp net mvc 4