Подтвердить что ты не робот

WebAPi - унифицировать формат сообщений об ошибках от ApiController и OAuthAuthorizationServerProvider

В моем проекте WebAPI я использую Owin.Security.OAuth, чтобы добавить проверку подлинности JWT. Внутри GrantResourceOwnerCredentials моего OAuthProvider я устанавливаю ошибки, используя следующую строку:

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

это возвращается клиенту как:

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

после того, как пользователь получит аутентификацию, и он пытается выполнить "обычный" запрос на один из моих контроллеров, он получает ниже ответа, когда модель недействительна (с использованием FluentValidation):

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

Оба запроса возвращают 400 Bad Request, но иногда вы должны искать поле error_description, а иногда и для message

Я смог создать пользовательское ответное сообщение, но это относится только к результатам, которые я возвращаю.

Мой вопрос: возможно ли заменить message на error в ответ, который возвращается ModelValidatorProviders и в других местах?

Я читал о ExceptionFilterAttribute, но я не знаю, хорошо ли это для начала. FluentValidation не должно быть проблемой, потому что все, что он делает, заключается в добавлении ошибок в ModelState.

EDIT:
Следующее, что я пытаюсь исправить, - это несогласованное соглашение об именах в возвращаемых данных через WebApi - при возврате ошибки из OAuthProvider у нас есть error_details, но при возврате BadRequest с ModelState (из ApiController) мы имеем ModelState. Как вы видите, сначала использует snake_case и второй camelCase.

4b9b3361

Ответ 1

ОБНОВЛЕННЫЙ ОТВЕТ (используйте промежуточное программное обеспечение)

Так как оригинальная идея обработчика делегирования веб-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(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;
        }
    }
}

... и зарегистрировались на ранней стадии конвейера до того, как будут добавлены посредники проверки подлинности и обработчики веб-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. Поскольку они вложены, ответное сообщение перемещается в другом направлении. То есть последний обработчик первый, чтобы получить ответное сообщение.

Источник: Обработчики HTTP-сообщений в веб-интерфейсе ASP.NET

Ответ 2

можно заменить сообщение с ошибкой в ​​ответ, что возвращается ModelValidatorProviders

Мы можем использовать перегруженный SetError, чтобы сделать это иначе, замените ошибку сообщением.

BaseValidatingContext<TOptions>.SetError Method (String)

Отмечает этот контекст как не проверенный приложением и присваивает различные свойства информации об ошибке. HasError становится истинным, а IsValidated становится ложным в результате вызова.

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