Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebAPi - unify error messages format from ApiController and OAuthAuthorizationServerProvider

Tags:

In my WebAPI project I'm using Owin.Security.OAuth to add JWT authentication. Inside GrantResourceOwnerCredentials of my OAuthProvider I'm setting errors using below line:

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

this is returned to client as:

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

after user gets authenticated and he tries to do "normal" request to one of my controllers he gets below response when model is invalid (using FluentValidation):

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

Both requests are returning 400 Bad Request, but sometimes You must look for error_description field and sometimes for message

I was able to create custom response message, but this only applies to results I'm returning.

My question is: is it possible to replace message with error in response that is returned by ModelValidatorProviders and in other places?

I've read about ExceptionFilterAttribute but I don't know if this is a good place to start. FluentValidation shouldn't be a problem, because all it does is adding errors to ModelState.

EDIT:
Next thing I'm trying to fix is inconsistent naming convention in returned data across WebApi - when returning error from OAuthProvider we have error_details, but when returning BadRequest with ModelState (from ApiController) we have modelState. As You can see first uses snake_case and second camelCase.

like image 474
Misiu Avatar asked Jan 05 '17 13:01

Misiu


People also ask

What are the different ways to handle errors in Web API?

You can customize how Web API handles exceptions by writing an exception filter. An exception filter is executed when a controller method throws any unhandled exception that is not an HttpResponseException exception.

How do I return an exception from Web API?

Return InternalServerError for Handled Exceptionscs file and locate the Get(int id) method. Add the same three lines within a try... catch block, as shown in Listing 2, to simulate an error. Create two catch blocks: one to handle a DivideByZeroException and one to handle a generic Exception object.

How do I use exception filter in Web API?

To apply the exception filter to all Web API controllers, the filter needs to register to GlobalConfiguration. Configuration. Filters collection. Following is a snapshot of Fiddler, when unhandled execution occurred in the action method.


1 Answers

UPDATED ANSWER (Use Middleware)

Since the Web API original delegating handler idea meant that it would not be early enough in the pipeline as the OAuth middleware then a custom middleware needs to be created...

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

...and registered early in the pipeline before the the authentication middlewares and web api handlers are added.

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

This example is just a basic start. It should be simple enough able to extend this starting point.

Though in this example the common model looks like what is returned from OAuthProvider, any common object model can be used.

Tested it with a few In-memory Unit Tests and through TDD was able to get it working.

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

ORIGINAL ANSWER (Use DelegatingHandler)

Consider using a DelegatingHandler

Quoting from an article found online.

Delegating handlers are extremely useful for cross cutting concerns. They hook into the very early and very late stages of the request-response pipeline making them ideal for manipulating the response right before it is sent back to the client.

This example is a simplified attempt at the unified error message for HttpError responses

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

Though this example is very basic, it is trivial to extend it to suit your custom needs.

Now it is just a matter of adding the handler to the pipeline

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

Message handlers are called in the same order that they appear in MessageHandlers collection. Because they are nested, the response message travels in the other direction. That is, the last handler is the first to get the response message.

Source: HTTP Message Handlers in ASP.NET Web API

like image 64
Nkosi Avatar answered Oct 05 '22 21:10

Nkosi