I'm developing a Web API 2 application and I'm currently trying to format error resposnes in a uniform way (so that the consumer will also know what data object/structure they can inspect to get more info about the errors). This is what I've got so far:
{
"Errors":
[
{
"ErrorType":5003,
"Message":"Error summary here",
"DeveloperAction":"Some more detail for API consumers (in some cases)",
"HelpUrl":"link to the docs etc."
}
]
}
This works fine for exceptions thrown by the application itself (i.e inside controllers). However, if the user requests a bad URI (and gets a 404) or uses the wrong verb (and gets a 405) etc, Web Api 2 spits out a default error message e.g.
{
Message: "No HTTP resource was found that matches the request URI 'http://localhost/abc'."
}
Is there any way of trapping these kinds of errors (404, 405 etc.) and formatting them out into the error response in the first example above?
So far I've tried:
Custom ExceptionAttribute inherting ExceptionFilterAttribute
Custom ControllerActionInvoker inherting ApiControllerActionInvoker
IExceptionHandler (new Global Error Handling feature from Web API 2.1)
However, none of these approaches are able to catch these kinds of errors (404, 405 etc). Any ideas on how/if this can be achieved?
...or, am I going about this the wrong way? Should I only format error responses in my particular style for application/user level errors and rely on the default error responses for things like 404?
You can override the DelegatingHandler abstract class and intercept the response to the client. This will give you the ability to return what you want.
Here's some info on it.
http://msdn.microsoft.com/en-us/library/system.net.http.delegatinghandler(v=vs.118).aspx
Here's a poster of the Web Api pipeline that shows what can be overriden.
http://www.asp.net/posters/web-api/asp.net-web-api-poster.pdf
Create a Handler class like this to override the response
public class MessageHandler1 : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
Debug.WriteLine("Process request");
// Call the inner handler.
var response = base.SendAsync(request, cancellationToken);
Debug.WriteLine("Process response");
if (response.Result.StatusCode == HttpStatusCode.NotFound)
{
//Create new HttpResponseMessage message
}
;
return response;
}
}
In your WebApiConfig.cs class add the handler.
config.MessageHandlers.Add(new MessageHandler1());
UPDATE
As Kiran mentions in the comments you can use the OwinMiddleware to intercept the response going back to the client. This would work for MVC and Web Api running on any host.
Here's an example of how to get the response and change it as it goes to the client.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Use(typeof(MyMiddleware));
}
}
public class MyMiddleware : OwinMiddleware
{
public MyMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if(context.Response.StatusCode== 404)
{
context.Response.StatusCode = 403;
context.Response.ReasonPhrase = "Blah";
}
}
}
I have done in same way as #Dan H mentioned
public class ApiGatewayHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
var objectContent = response.Content as ObjectContent;
return await Task.FromResult(new ApiResult(HttpStatusCode.NotFound, VmsStatusCodes.RouteNotFound, "", objectContent == null ? null : objectContent.Value).Response());
}
return response;
}
catch (System.Exception ex)
{
return await Task.FromResult(new ApiResult(HttpStatusCode.BadRequest, VmsStatusCodes.UnHandledError, ex.Message, "").Response());
}
}
}
Added routing like below and now it hits the try catch for invalid url
config.Routes.MapHttpRoute(name: "DefaultApi",routeTemplate: "api/{controller}/{id}",defaults: new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute(name: "NotFound", routeTemplate: "api/{*paths}", defaults: new { controller = "ApiError", action = "NotFound" });
Related
I am getting a strange error when calling an API from other API using Refit library.
FYI: My APIs are built using .Net 5.
The First API returns 200 response it is just the Refit which is unable to show the response.
I have checked the types of the response and model which are compatible.
First API
The declaration of the endpoint:
[Get("/projects/private/list")]
[Headers("Authorization: Bearer")]
Task<List<ProjectsResponse>> GetProjects([Body] ProjectsRequest projectIds);
The definition:
[HttpGet("private/list")]
[ProducesResponseType(statusCode: (int)HttpStatusCode.OK, type: typeof(IEnumerable<ProjectsResponse>))]
public async Task<IActionResult> GetProjects([FromBody] ProjectsRequest request)
{
IEnumerable<Project> projects = await _mediator.Send(new GetProjectsQuery { ProjectIds = request.ProjectIds });
return Ok(_mapper.Map<List<ProjectsResponse>>(projects));
}
The request model:
public class ProjectsRequest
{
public string[] ProjectIds { get; set; }
}
The response model:
public class ProjectsResponse
{
public string Id { get; set; }
public string Name { get; set; }
}
Second API
The call:
List<ProjectsResponse> projects = await _otherApi.GetProjects(new ProjectsRequest
{
ProjectIds = projectIds.ToArray()
});
Basically I am returning list of objects in the response which is causing this problem.
The strange behavior is that when I log the Request.Content it works fine but when the log is removed the 502 error starts popping.
So, when I add _logger.LogInformation("Request:{data}", await request.Content.ReadAsStringAsync()); everything works correctly but when it is removed the Refit starts throwing 502.
I have done the token handling using the DelegatingHandler of the Refit.
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
var token = await GetAccessTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
//_logger.LogInformation("Request:{data}", await request.Content.ReadAsStringAsync());//Uncommenting this line makes it work.
HttpResponseMessage httpResponseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
//_logger.LogInformation("Response:{data}", await httpResponseMessage.Content.ReadAsStringAsync());
return httpResponseMessage;
}
catch (ApiException ex)
{
_logger.LogError(ex, ex.Message);
throw;
}
}
I have created a new Exception middleware in my .Net Core application. All the exceptions throughout the application are captured and logged here. What I want is to return a IActionResult type like InternalServerError() or NotFound() from the Exception Middleware and not do response.WriteAsync as below.
Controller Method:
public async Task<IActionResult> Post()
{
//Do Something
return Ok();
}
Middleware:
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
public ExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var response = context.Response;
var statusCode = (int)HttpStatusCode.InternalServerError;
var message = exception.Message;
var description = exception.Message;
response.ContentType = "application/json";
response.StatusCode = statusCode;
await response.WriteAsync(JsonConvert.SerializeObject(new ErrorResponse
{
Message = message,
Description = description
}));
}
}
IActionResult is a thing from MVC, so it is only available within the MVC pipeline (including Razor Pages). Just before the MVC middleware terminates, it will execute those action results using ExecuteResultAsync. That method is then responsible of writing that response to HttpContext.Response.
So in custom middleware, you cannot just set an action result and have it executed, since you are not running within the MVC pipeline. However, with that knowledge, you can simply execute the result yourself.
Let’s say you want to execute a NotFoundResult which is what Controller.NotFound() creates. So you create that result and call ExecuteResultAsync with an . That executor will be able to execute that result object and write to the response:
var result = new NotFoundResult();
await result.ExecuteResultAsync(new ActionContext
{
HttpContext = context
});
That's not really possible due to where IActionResult and middleware sit in relation to one another in the architecture. Middleware sits much lower, and so it can't reach further up the stack to IActionResult. Here's an answer that talks more about it: https://stackoverflow.com/a/43111292/12431728
What you're trying to do can be done by simply adding this line:
app.UseStatusCodePagesWithReExecute("/Public/Error", "?statusCode={0}");
to the Configure method in the Startup.cs. Then you can create your Public Controller with Error method that does the following:
[AllowAnonymous]
public IActionResult Error(int? statusCode = null)
{
// Retrieve error information in case of internal errors.
var error = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error;
var path = HttpContext.Features.Get<IExceptionHandlerPathFeature>()?.Path;
// TODO: Redirect here based on your status code or perhaps just render different views for different status codes.
}
There is also another middleware that allows you to do a similar thing:
app.UseStatusCodePages(async context =>
{
if (context.HttpContext.Response.StatusCode == 401)
{
context.HttpContext.Response.Redirect("Errors/Unauthorized/");
}
else if (context.HttpContext.Response.StatusCode == 500)
{
// TODO: Redirect for 500 and so on...
}
});
Until now, I had a GET method that looked like the following:
protected override async Task<IHttpActionResult> GetAll(QueryData query)
{
// ... Some operations
//LINQ Expression based on the query parameters
Expression<Func<Entity, bool>> queryExpression = BuildQueryExpression(query);
//Begin to count all the entities in the repository
Task<int> countingEntities = repo.CountAsync(queryExpression);
//Reads an entity that will be the page start
Entity start = await repo.ReadAsync(query.Start);
//Reads all the entities starting from the start entity
IEnumerable<Entity> found = await repo.BrowseAllAsync(start, queryExpression);
//Truncates to page size
found = found.Take(query.Size);
//Number of entities returned in response
int count = found.Count();
//Number of total entities (without pagination)
int total = await countingEntities;
return Ok(new {
Total = total,
Count = count,
Last = count > 0 ? GetEntityKey(found.Last()) : default(Key),
Data = found.Select(e => IsResourceOwner(e) ? MapToOwnerDTO(e) : MapToDTO(e)).ToList()
});
}
This worked like a charm and it was good. However, I was told recently to send the response metadata (that is, Total, Count and Last properties) as response custom headers instead of the response body.
I cannot manage to access the Response from the ApiController. I thought of a filter or attribute, but how would I get the metadata values?
I can keep all this information on the response and then have a filter that will deserialize the response before being sent to the client, and create a new one with the headers, but that seems troublesome and bad.
Is there a way to add custom headers directly from this method on an ApiController?
You can explicitly add custom headers in a method like so:
[HttpGet]
[Route("home/students")]
public HttpResponseMessage GetStudents()
{
// Get students from Database
// Create the response
var response = Request.CreateResponse(HttpStatusCode.OK, students);
// Set headers for paging
response.Headers.Add("X-Students-Total-Count", students.Count());
return response;
}
For more information read this article: http://www.jerriepelser.com/blog/paging-in-aspnet-webapi-http-headers/
I have entered comments, here is my complete answer.
You will need to create a custom filter and apply that to your controller .
public class CustomHeaderFilter : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
var count = actionExecutedContext.Request.Properties["Count"];
actionExecutedContext.Response.Content.Headers.Add("totalHeader", count);
}
}
In your Controller
public class AddressController : ApiController
{
public async Task<Address> Get()
{
Request.Properties["Count"] = "123";
}
}
Simple solution is to write just this:
HttpContext.Current.Response.Headers.Add("MaxRecords", "1000");
What you need is:
public async Task<IHttpActionResult> Get()
{
var response = Request.CreateResponse();
response.Headers.Add("Lorem", "ipsum");
return base.ResponseMessage(response);
}
I hope this answers your question.
Alternatively, it’s better to leverage DelegatingHandler if it is something you need to perform on every response. Because it will work on the request/response pipeline and not on the controller/action level. In my case I must add some headers with every response, so I did what I described. See code snippet below
public class Interceptor : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
response.Headers.Add("Access-Control-Allow-Origin", "*");
response.Headers.Add("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,PUT,OPTIONS");
response.Headers.Add("Access-Control-Allow-Headers", "Origin, Content-Type, X-Auth-Token, content-type");
return response;
}
}
And you would be requiring to add this handler in WebApiConfig
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MessageHandlers.Add(new Interceptor());
}
}
You can use a custom ActionFilter that will allow you to send custom headers and access the HttpContext:
public class AddCustomHeaderFilter : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
actionExecutedContext.Response.Content.Headers.Add("name", "value");
}
}
I have a webapi 2.0 project. I am registering two delegating handler in webapiconfig.cs. Sometimes it throws error "Index was out of bounds of the array" and the stact trace shows - error at System.Collections.GenericList.Add(T item) at handler1 at handler2. I am also using unityconfig that i am registering at Application_Start in Global.asax and I am registering WebApiconfig at Application_Start in Global.asax
I dont know why such strange behavior as it is working fine for most of the time but sometimes it throws this error.
My Code in WebApiconfig.cs is as below -
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
config.EnableCors();
config.MessageHandlers.Add(new Handler1(););
config.MessageHandlers.Add(new Handler2());
// Web API routes
config.MapHttpAttributeRoutes();
}
Code for two handlers are as below -
public class Handler1 : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string routeTemplate = ((IHttpRouteData[])request.GetConfiguration().Routes.GetRouteData(request).Values["MS_SubRoutes"])
.First().Route.RouteTemplate.ToString();
IPrincipal principal = new GenericPrincipal(new GenericIdentity(new Guid()), new string[] { "myRole" });
HttpContext.Current.User = principal;
return base.SendAsync(request, cancellationToken).ContinueWith(
(task) =>
{
HttpResponseMessage response = task.Result;
return response;
}
);
}
}
public class Handler2 : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string routeTemplate = ((IHttpRouteData[])request.GetConfiguration().Routes.GetRouteData(request).Values["MS_SubRoutes"])
.First().Route.RouteTemplate.ToString();
HttpConfiguration config = request.GetConfiguration();
config.Filters.Add(new Filter1());
return base.SendAsync(request, cancellationToken).ContinueWith(
(task) =>
{
HttpResponseMessage response = task.Result;
return response;
}
);
}
}
Can someone please help with what I should do?
This line looks very suspicious
string routeTemplate = ((IHttpRouteData[])request.GetConfiguration()
.Routes.GetRouteData(request)
.Values["MS_SubRoutes"])
.First()
.Route.RouteTemplate.ToString();
Because "MS_SubRoutes" thing could not exists, so you need to break that codeion different blocks to validate. Also you need to be sure that the collection has at least one member so you need to control this issue too.
var ms_SubRoutes = (IHttpRouteData[])request
.GetConfiguration()
.Routes
.GetRouteData(request)
.Values["MS_SubRoutes"];
string routeTemplate;
//Verify if "MS_SubRoutes" are part of the request
if (ms_SubRoutes != null)
{
try
{
routeTemplate = ms_SubRoutes.First()
.Route
.RouteTemplate.ToString();
}
catch(IndexOutOfRangeException ior)
{
//Do something
throw;
}
catch (Exception ex)
{
//Do something
throw;
}
}
The problem was add filters inside handler.
config.Filters.Add(new Filter1());
Added the filter in GlobalConfig and everything is running smoothly.
How comes that a custom ExceptionHandler is never called and instead a standard response (not the one I want) is returned?
Registered like this
config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger());
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
and implemented like this
public class GlobalExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new ExceptionResponse
{
statusCode = context.Exception is SecurityException ? HttpStatusCode.Unauthorized : HttpStatusCode.InternalServerError,
message = "An internal exception occurred. We'll take care of it.",
request = context.Request
};
}
}
public class ExceptionResponse : IHttpActionResult
{
public HttpStatusCode statusCode { get; set; }
public string message { get; set; }
public HttpRequestMessage request { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(statusCode);
response.RequestMessage = request;
response.Content = new StringContent(message);
return Task.FromResult(response);
}
}
and thrown like this (test)
throw new NullReferenceException("testerror");
in a controller or in a repository.
UPDATE
I do not have another ExceptionFilter.
I found a trigger for this behavior:
Given URL
GET http://localhost:XXXXX/template/lock/someId
sending this header, my ExceptionHandler works
Host: localhost:XXXXX
sending this header, it doesn't work and the built-in handler returns the error instead
Host: localhost:XXXXX
Origin: http://localhost:YYYY
This might be an issue with CORS requests (I use the WebAPI CORS package globally with wildcards) or eventually my ELMAH logger. It also happens when hosted on Azure (Websites), though the built-in error handler is different.
Any idea how to fix this?
Turns out the default only handles outermost exceptions, not exceptions in repository classes. So below has to be overridden as well:
public virtual bool ShouldHandle(ExceptionHandlerContext context)
{
return context.ExceptionContext.IsOutermostCatchBlock;
}
UPDATE 1
WebAPI v2 does not use IsOutermostCatchBlock anymore. Anyway nothing changes in my implementation, since the new code in ShouldHandle still prevents my Error Handler. So I'm using this and my Error Handler gets called once. I catch errors in Controllers and Repositories this way.
public virtual bool ShouldHandle(ExceptionHandlerContext context)
{
return true;
}
UPDATE 2
Since this question got so much attention, please be aware that the current solution is the one linked by #JustAMartin in the comments below.
The real culprit here is CorsMessageHandler inserted by EnableCors method in message processing pipline. The catch block intercept any exception and convert into a response before it can reach the HTTPServer try-catch block and ExceptionHandler logic can be invoked
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
CorsRequestContext corsRequestContext = request.GetCorsRequestContext();
HttpResponseMessage result;
if (corsRequestContext != null)
{
try
{
if (corsRequestContext.IsPreflight)
{
result = await this.HandleCorsPreflightRequestAsync(request, corsRequestContext, cancellationToken);
return result;
}
result = await this.HandleCorsRequestAsync(request, corsRequestContext, cancellationToken);
return result;
}
catch (Exception exception)
{
result = CorsMessageHandler.HandleException(request, exception);
return result;
}
}
result = await this.<>n__FabricatedMethod3(request, cancellationToken);
return result;
}