I'm interested in knowing what are the best practices being followed to raise exceptions in the ODataController.
If you raise an exception in the method it is translated to response code of 500 by default and the content has details on the error. I would like to be explicit of the response code and send 400 in cases of invalid key.
For example: If the input request has an invalid key would like to return a HttpResponseCode of 400 and content should have the error details similar to raising an exception.
Thanks a lot for your inputs
OData (at least since v3) uses specific json to represent errors:
{
"error": {
"code": "A custom error code",
"message": {
"lang": "en-us",
"value": "A custom long message for the user."
},
"innererror": {
"trace": [...],
"context": {...}
}
}
}
Microsoft .Net contains Microsoft.Data.OData.ODataError and Microsoft.Data.OData.ODataInnerError classes to form OData error on a server side.
To form proper OData error response (HttpResponseMessage), that contains error details you can:
1) form and return HttpResponseMessage in controller's action using System.Web.OData.Extensions.HttpRequestMessageExtensions.CreateErrorResponse method
return Request.CreateErrorResponse(HttpStatusCode.Conflict, new ODataError { ErrorCode="...", Message="...", MessageLanguage="..." }));
2) throw HttpResponseException using the same method for creating HttpResponseMessage
throw new HttpResponseException(
Request.CreateErrorResponse(HttpStatusCode.NotFound, new ODataError { ErrorCode="...", Message="...", MessageLanguage="..." }));
3) throw custom typed exception and convert it using Web Api action filters
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is CustomException)
{
var e = (CustomException)context.Exception;
var response = context.Request.CreateErrorResponse(e.StatusCode, new ODataError
{
ErrorCode = e.StatusCodeString,
Message = e.Message,
MessageLanguage = e.MessageLanguage
});
context.Response = response;
}
else
base.OnException(context);
}
}
Use HttpResponseException,
e.g. throw new HttpResponseException(HttpStatusCode.NotFound);.
Details can be found here.
For ASP.NET Core with OData, replace the EnableQueryAttribute on your Get method with a custom attribute that catches ODataException and throws a custom exception. In most cases, this allows standard error handling to step in as expected. Originally found this solution at https://github.com/OData/WebApi/issues/1898.
For your custom attribute, use something like the following:
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
public override void ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions)
{
try
{
base.ValidateQuery(request, queryOptions);
}
catch (ODataException e)
{
throw new CustomException(e.Message, e){UserMessage = "Invalid OData query."};
}
}
}
On your Get method, use something like the following:
[HttpGet, CustomEnableQuery]
public virtual IQueryable<TDomainDto> Get()
{
return Repository.Get();
}
Check CreateErrorResponse in the OData documentation. The namespace to the used in Microsoft.AspNet.OData. I got my code working using this.
Related
How to catch this error in the back-end C# code?
See the solution usedat the end of the question description.
It is due to integer overflow for a HTTP GET URL where the URL contains an integer parameter. Calls with a 32 bit integer sized value work (e.g., "1234" works and the API is called).
HTTP GET www.domain.net/api/product/{productIdInteger}
Where the JavaScript caller calls HTTPS
www.domain.net/api/product/222222222222222222222222222222222222
The response looks like:
{
"errors": {
"productIdInteger": [
"The value '222222222222222222222222222222222222' is not valid."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-f6a8a724853f3f44ba9fc6468a42ad94-aca067537e73b645-00"
}
Startup.cs adds a middleware to catch exceptions at the WebAPI controller level.
This does not catch the URL parameter parsing error or a bad JSON in HTTP Body parsing error
Startup.cs has app.UseMiddleware();
MyErrorMiddleware has this invoke method and MyExceptionHandlerAsync is never called for this case.
public async Task Invoke(HttpContext context)
{
try
{
//This line gets called
await _next(context);
}
catch (Exception ex)
{
//The catch block is not executed if the integer passed to the controller method overflows a 32 bit integer
//The catch block is executed if an C# WebAPI controller method is called and the controller method throws an exception
await MyExceptionHandlerAsync(context, ex);
}
}
Here is what worked best for our application.
Create base class for controller methods (code below) having a Boolean errorFlag and a List ErrorList
Derive the return objects for controller methods off the base class in #1. Each of the return classes had the error properties plus the data returned from the controller method. The data property returned for the controller would be called "ReturnData" to make the front-end JavaScript easier.
Add the model validation error handler to Startup.cs. Return the same base class as in step #1
Add in the error handling middleware catch block, code to return the error using the same error class from step #1
This handles 1) business error from controller (not a thrown exception), 2) exception thrown from controller or a method it calls, no need to have a try/catch in the controller, 3) error passing data to the WebAPI controller method (model validation error)
public class ReturnBaseClass
{
public ReturnBaseClass() { errorList = new List<string>()}
public bool errorFlag {get;set;}
public List<string> errorList {get;set;}
}
//A controller return data could look like
public class CourseDataReturn : ReturnBaseClass
{
//Always called ReturnData to make front-end JavaScript easier
public CourseInfo ReturnData {get;set;}
}
//CourseInfo is the data returned from the data repository method called from the controller.
public class CourseInfo
{
public int CourseId {get;set;}
public string CourseName {get;set;}
}
We simplified our return HTTP status codes to OK, Bad Request, Internal Server Error for back-end WebAPI calls which are made. All other HTTP status codes would be a HTTP transport error (timeout, not found (URL is bad), ....
That got us past the REST mismatch of HTTP NotFound being either the URL is not found, such as domain name does not exist, and the database row being retrieved for a HTTP GET student by student ID not finding the student.
It is not an exception. It is ModelState not valid.
So you can catch this error by:
Step 1:
Add ConfigureApiBehaviorOptions in Program.cs file:
builder.Services.AddControllers().ConfigureApiBehaviorOptions(options => {
options.SuppressModelStateInvalidFilter = true;
});
Step 2:
Handle errors in top of Controller file:
if (!ModelState.IsValid) {
var errors = new List<string>();
foreach (var state in ModelState)
{
foreach (var error in state.Value.Errors)
{
errors.Add(error.ErrorMessage);
}
}
// Do something to handle error list
}
The code below is based on [https://briancaos.wordpress.com/2019/11/29/net-core-catch-model-binding-exceptions/]
We used a variation on this code in the solution
//In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
... //all of the configuration for services, etc...
//Add this at the very end of ConfigureServices()
//Handle errors - model bind error, bad parameter passed to WebAPI endpoint, ...
services.Configure<ApiBehaviorOptions>(options =>
options.InvalidmsResponseFactory = (ac =>
{
var ms = ac.ms;
//Properties related to errors: ms.IsValid, s.ErrorCount, ms.ValidationState
//var errors = ms.Where(x => x.Value.Errors.Count > 0);
var errs = new ValidationProblemDetails(ac.ms);
var errList = new List<string>();
//errList.Add($"Detail: {errs.Detail}");
errList.Add($"Title: {errs.Title}");
//errList.Add($"URL: {errs.Instance}");
//errList.Add($"HTTP Status Code: {errs.Status}");
//errList.Add($"Error Type: {errs.Type}");
List<string> mlist = new List<string>();
foreach (string k in errs.Errors.Keys)
{
//build one message line <KEY>: <ERROR MESSAGE 1>, <ERROR MESSAGE 2>, ...
string m = $"{k}: ";
string[] value = errs.Errors[k];
if (value.Length > 0)
{
m = $"{m}{string.Join(",", value)}";
}
mlist.Add(m);
}
string r = "";
if (msgList.Any())
{
r = string.Join(",", msgList);
}
return new BadRequestObjectResult(r);
}
));
I am not able to find how to throw exceptions as they are generated by .NET 6 Web API.
If I return BadRequest(ModelState) with added errors I am not getting same message with status, type, title etc.
By default .NET generates this kind of errors when validation error occurs:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-488f8c0057223cbba92fc1fbfc8865d8-2341d7aba29d098f-00",
"errors": {
"$": [
"The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 7 | BytePositionInLine: 0."
],
"model": [
"The model field is required."
]
}
}
I want to configure my application to respond with the same error JSON, or I want to configure so it will respond with the custom JSON fields.
I tried to add a middleware that will catch exceptions, but it does not handle model errors (which are handled by framework by itself). How can I handle errors globally, or how can I throw exceptions that will be treated the same as framework handles them? Any documentation/tutorial links are welcome!
You can disable default bad request responses like the following code:
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
So you can return any model you want in BadRequest.
However, you then have to do the model validation yourself in each endpoint like:
[HttpPost(Name = "Post1")]
public IActionResult Post1(Model1 model)
{
if (!ModelState.IsValid)
return BadRequest(new CustomApiResponse());
...
}
[HttpPost(Name = "Post2")]
public IActionResult Post2(Model2 model)
{
if (!ModelState.IsValid)
return BadRequest(new CustomApiResponse());
...
}
If you want to return a global JSON structure, you can create a filter with the ActionFilterAttribute and use it for all your endpoints so you don't need to do model validation on every endpoint.
Custom Validation Filter:
public class CustomValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new GlobalApiResponse());
}
base.OnActionExecuting(context);
}
}
You need to register your custom filter in Program.cs
builder.Services.AddControllers(options =>
{
options.Filters.Add(typeof(CustomValidationFilterAttribute));
});
This is just one of the ways you can use to achieve what you want, you can find different methods on the internet.
In .Net6 Web Api, If You Want To Validate Model By Your Code, And You Can Debug It, Just Do This:
public class ModelValidationFilterAttribute : ActionFilterAttribute
{
public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new BaseResponse() { code = ResponseCode.失败, msg = "" });
}
return base.OnActionExecutionAsync(context, next);
}
}
Then Do It In The Program.cs
// Add services to the container.
builder.Services.AddControllers(options =>
{
//Add Your Filter
options.Filters.Add<ModelValidationFilterAttribute>();
}).ConfigureApiBehaviorOptions(options =>
{
//Disable The Default
options.SuppressModelStateInvalidFilter = true;
});
I have created by some example the ErrorController, which is handling Exception. Currently I have this:
[Route("[controller]")]
[ApiController]
[Authorize]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorController : ControllerBase
{
public IActionResult ServerError()
{
var feature = HttpContext.Features.Get<IExceptionHandlerFeature>();
ErrorResponse response;
if (feature != null && feature.Error.GetType() == typeof(HttpResponseException))
{
response = new ErrorResponse
{
Error = "Error during processing request",
Message = feature.Error.Message
};
HttpContext.Response.StatusCode = ((HttpResponseException) feature.Error).HttpCode;
}
else
{
response = new ErrorResponse
{
Error = "Unexpected Server Error",
Message = feature?.Error.Message
};
}
return Content(JsonSerializer.Serialize(response), "application/json");
}
}
So whenever I throw in my method in controllers HttpResponseException, It will read it, and create response with corresponding code. But doing it through this, will log the HttpResponseException, which is not desired behaviour.
I have found solutions with Request.CreateResponse(), but that method does not exists, but when I replicated this method by myself, it is not desired behaviour (because of Swashbuckle/Swagger UI - The returning type is not the model object, but the HttpResponseMessage).
Found also something about ExceptionFilterAttribute, where I produced this code:
public class HttpExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
if (!(context.Exception is HttpResponseException)) return;
var exception = (HttpResponseException) context.Exception;
context.Result = new ObjectResult(""){StatusCode = exception.HttpCode};
}
}
But can't tell where to globally registered (based on this article).
So how to correctly manage returning desired object or some error with code, in a way that it will not be logged as warn?
Filters are registered globally in the Startup.cs via the options of AddMVC or AddControllersWithViews options.
Another way to handle exceptions globally is using an exception handling middleware which would catch also unexpected exceptions (which is my preferred way).
I want to add an ID (GUID) to the exception and:
Log it
Return it to the client json response
Where should I generate this log ID value and add it to the exception message that is logged. And where to change the following default response?
{
"targetUrl": null,
"result": null,
"success": false,
"error": {
"message": "An internal error occurred during your request!",
"details": "..."
},
"unAuthorizedRequest": false
}
I am using .NET Core version.
If you want to disable displaying the message for a particular AJAX call, add abpHandleError: false into the abp.ajax options.
Or you can disable the default behavior of the framework exception wrapper
public class PeopleController : AbpController
{
[HttpPost]
[WrapResult(WrapOnSuccess = false, WrapOnError = false)]
public JsonResult SavePerson(SavePersonModel person)
{
//TODO: save new person to database and return new person's id
return Json(new {PersonId = 42});
}
}
https://aspnetboilerplate.com/Pages/Documents/Javascript-API/AJAX?searchKey=wrap#asp-net-mvc-controllers
Another thing is; you can send exception details to the client by the below configuration
...
using Abp.Web.Configuration;
...
public override void PreInitialize()
{
Configuration.Modules.AbpWebCommon().SendAllExceptionsToClients = true;
}
...
https://aspnetboilerplate.com/Pages/Startup-Configuration#configuring-modules
Result Wrapping & Exception Handling:
ASP.NET Boilerplate does not wrap Web API actions by default if an action has successfully executed. It, however, handles and wraps exceptions. You can add the WrapResult/DontWrapResult attributes to actions and controllers for finer control. You can change this default behavior from the startup configuration (using Configuration.Modules.AbpWebApi()...). See the AJAX document for more info about result wrapping.
https://aspnetboilerplate.com/Pages/Documents/Web-API-Controllers?searchKey=wrap#result-wrapping-exception-handling
Wrapping Results
ASP.NET Boilerplate wraps the return values of dynamic Web API actions using an AjaxResponse object. See the ajax documentation for more information on wrapping. You can enable/disable wrapping per method or per application service. See this example application service:
public interface ITestAppService : IApplicationService
{
[DontWrapResult]
DoItOutput DoIt(DoItInput input);
}
https://aspnetboilerplate.com/Pages/Documents/Dynamic-Web-API?searchKey=wrap#wrapping-results
Lastly you can write your own ResultWrapperHandler...
public class CustomResultWrapperHandler : ResultWrapperHandler, ITransientDependency
{
//...
protected override void WrapResultIfNeeded(HttpRequestMessage request, HttpResponseMessage response)
{
//...
base.WrapResultIfNeeded(request, response);
}
}
if you want to get special message in some case you can use
throw new UserFriendlyException("your message");
the above code just effects on error message and doesn't show the details.
so its good option for production version.
I have received the assignment to make our C# Webapi return always error responses with the following format:
{
"error": {
"code": 15,
"message": "My custom error message"
}
}
Because of that I registered my own ExceptionFilterAttribute:
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
class CustomError
{
public int code { get; set; }
public String message { get; set; }
}
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is BaseException)
{
BaseException exception = (BaseException)context.Exception;
HttpError error = new HttpError();
CustomError customError = new CustomError
{
code=exception.CustomError.code,
message=exception.CustomError.message
};
error.Add("error", customError);
context.Response = context.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, error);
}
else
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
}
}
This is working very well when the exception was throwed in a controller. But if the exception was throwed in an attribute (AuthorizeAttribute or EnableQueryAttribute), althoug my custom ExceptionFilter is called and the same code is executed, the response generated ignores the given HttpError and the response has the following body:
{
"error": {
"code": "",
"message": ""
}
}
I'm not very skilled in c#, I'm pretty sure that I'm doing something wrong, but I don't know what :S
Thanks in advance.
Edit 1:
I'm applying the Attribute that throws the exception at each method where the attribute is needed. For example I have an Odata Controller for an entity called "event":
[CustomAuthentication]
[CustomEnableQueryAttribute(PageSize = 20)]
public IQueryable<Event> Get()
{
(...)
return result;
}
As said, if the exception is throwed in the body of the controller, then my CustomExceptionFilter is invoked, and the custom response is created correctly.
But if the exception is thrown in the CustomAuthenticationAttribute or in the CustomEnableQueryAttribute, then altough my CustomExceptionFilter is invoked and the very same code is executed, the body response is wrong (see example).
Filter are mean for only controller but for global errors you need to use Global Error Filter in WebAPI.
to handle error thrown from attribute you need to Create Global Error Hnandler : https://learn.microsoft.com/en-us/aspnet/web-api/overview/error-handling/exception-handling
class OopsExceptionHandler : ExceptionHandler
{
public override void HandleCore(ExceptionHandlerContext context)
{
context.Result = new TextPlainErrorResult//you have to create this class
{
Request = context.ExceptionContext.Request,
Content = "Oops! Sorry! Something went wrong." +
"Please contact support#contoso.com so we can try to fix it."
};
}
}
there are a number of cases that exception filters can't handle. For example:
Exceptions thrown from controller constructors.
Exceptions thrown from message handlers.
Exceptions thrown during routing.
Exceptions thrown during response content serialization .