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
.
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With