在我的WebAPI项目中,我使用ClusterableStop
添加JWT身份验证。
在我的OAuthProvider的Owin.Security.OAuth
内,我使用以下行设置错误:
GrantResourceOwnerCredentials
将其作为以下内容返回给客户:
context.SetError("invalid_grant", "Account locked.");
在用户进行身份验证后,他尝试对我的某个控制器执行“正常”请求时,当模型无效时,他会得到以下响应(使用FluentValidation):
{
"error": "invalid_grant",
"error_description": "Account locked."
}
这两个请求都返回{
"message": "The request is invalid.",
"modelState": {
"client.Email": [
"Email is not valid."
],
"client.Password": [
"Password is required."
]
}
}
,但有时您必须查找400 Bad Request
字段,有时候查找error_description
I was able to create自定义响应消息,但这仅适用于我返回的结果。
我的问题是:是否可以在message
和其他地方返回的回复中将message
替换为error
?
我读过ModelValidatorProviders
但我不知道这是不是一个好的起点。 FluentValidation应该不是问题,因为它只是向ExceptionFilterAttribute
添加错误。
修改
接下来我要修复的是WebApi中返回数据的命名约定不一致 - 当从ModelState
返回错误时我们有OAuthProvider
,但是当error_details
返回BadRequest
时(来自ModelState
)我们ApiController
。如您所见,首先使用modelState
和第二snake_case
。
答案 0 :(得分:6)
更新的答案(使用中间件)
由于Web API原始委托处理程序的想法意味着它不像OAuth中间件那样在管道中足够早,因此需要创建自定义中间件......
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(stream).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;
}
}
}
...并在添加身份验证中间件和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返回的内容,但是可以使用任何常见的对象模型。
通过一些内存单元测试对其进行测试,并通过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<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");
}
}
}
原始答案(使用DelegatingHandler)
考虑使用DelegatingHandler
引自在线发现的文章。
委托处理程序对于横切关注点非常有用。 他们陷入了非常早期和非常晚期的阶段 请求 - 响应管道使它们成为操作的理想选择 在将其发回客户端之前做出回应。
此示例是针对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;
}
}
虽然这个示例非常基础,但扩展它以满足您的自定义需求是微不足道的。
现在只需将处理程序添加到管道
即可public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
config.MessageHandlers.Add(new HttpErrorHandler());
// Other code not shown...
}
}
消息处理程序的调用顺序与它们出现的顺序相同 MessageHandlers 集合。因为它们是嵌套的,所以响应消息以另一个方向传播。也就是说,最后一个处理程序是 第一个收到回复消息。
答案 1 :(得分:0)
是否可以在响应中替换带有错误的消息 由ModelValidatorProviders返回
我们可能会使用重载SetError来执行此操作,将错误替换为消息。
BaseValidatingContext<TOptions>.SetError Method (String)
将此上下文标记为未由应用程序验证,并分配各种错误信息属性。 HasError变为true,并且IsValidated因调用而变为false。
string msg = "{\"message\": \"Account locked.\"}";
context.SetError(msg);
Response.StatusCode = 400;
context.Response.Write(msg);