Base solution for your next web application
Open Closed

Customizing error handling #12422


User avatar
0
kylem created

Hello there,

I have a question in regards to error handling.

Right now, if our code throws UserFriendlyException and wraps certain error codes like for example 404 with it, we would get Error 500 and our original error code and message inside that 500 error payload, like here:

{
  "result": null,
  "targetUrl": null,
  "success": false,
  "error": {
    "code": 404,
    "message": "Internal error code: 404, message: object with id 12345 not found",
    "details": null,
    "validationErrors": null
  },
  "unAuthorizedRequest": false,
  "__abp": true
}

In this particular case we would like to get 404 returned to a client instead of 500. 404 is just one example, we would like to handle multiple http errors in this way.

What would be recommended approach for making the change? We would like that to be applied to requests handled by Controller and AppService layer.

Thanks, Peja

Markdown is supported
Copy & paste or drag & drop images (max 30 MB per image)

7 Answer(s)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @kylem

    You can use the [DontWrapResult] attribute in your controller or app service methods to prevent the error from being wrapped in a 500 response. This allows the original error code (like 404) to be returned to the client directly instead of a generic 500 error.

    [DontWrapResult]
    public async Task<IActionResult> SomeAction()
    {
        throw new UserFriendlyException(404, L("ObjectNotFound"));
    }
    

    By applying this attribute, your method will return the 404 error directly to the client, preserving the original HTTP status code.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    kylem created

    Thanks for the answer.

    I tried it and what I am getting in this case is still http 500 and now full exception trace has been returned to me, which is definitely not what I am looking for.

    What I would like to get back is this payload but with error code 404:

    {
      "result": null,
      "targetUrl": null,
      "success": false,
      "error": {
        "code": 404,
        "message": "Internal error code: 404, message: object with id 12345 not found",
        "details": null,
        "validationErrors": null
      },
      "unAuthorizedRequest": false,
      "__abp": true
    }
    

    From what I see, error handling is able to handle some type of errors. For example if I throw EntityNotFoundException instead of UserFriendlyException I would correctly get 404 back in the same payload:

    Also, if I search the code base, I can see that there are few places where exception handling logic exists which will deal with those 2 exceptions.

    My question is if you can recommend us correct place to add our own logic to support some custom scenarios we have.

    Thanks, Peja

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @kylem

    You’re absolutely right, by default, throwing a UserFriendlyException or a custom one like HttpStatusCodeException doesn't guarantee the correct HTTP status code is reflected in the final response payload. But we’ve implemented a clean and reusable solution to support custom error scenarios such as yours, while keeping the familiar ABP style wrapped response (AjaxResponse) with a specific HTTP status code like 404.

    To solve this, we introduced a custom exception type and a corresponding exception filter to return the correct HTTP status code and the expected ABP formatted response body:

    Custom Exception: HttpStatusCodeException

    This custom exception lets you specify the desired status code (like 404) and message:

    public class HttpStatusCodeException : UserFriendlyException
    {
        public int HttpStatusCode { get; }
    
        public HttpStatusCodeException(int httpStatusCode, string message)
            : base(message)
        {
            HttpStatusCode = httpStatusCode;
        }
    
        public HttpStatusCodeException(int httpStatusCode, string message, Exception innerException)
            : base(message, innerException)
        {
            HttpStatusCode = httpStatusCode;
        }
    }
    

    Custom Exception Filter: HttpStatusCodeExceptionFilter

    This filter checks for that specific exception and constructs a proper ABP-wrapped response (AjaxResponse) with the status code you defined.

    public class HttpStatusCodeExceptionFilter : IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            if (context.Exception is HttpStatusCodeException ex)
            {
                var responseCode = (ex.Code != 0) ? ex.Code : (ex.HttpStatusCode != 0 ? ex.HttpStatusCode : 500);
    
                var response = new AjaxResponse(
                    new ErrorInfo(responseCode, ex.Message)
                )
                {
                    Success = false
                };
    
                context.Result = new ObjectResult(response)
                {
                    StatusCode = responseCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    Then you simply register this globally in your startup:

     public IServiceProvider ConfigureServices(IServiceCollection services)
     {
        //...
        var builder = services.AddControllersWithViews(options =>
        {
            options.Filters.Add<HttpStatusCodeExceptionFilter>();
            //...
        });
        //...
    }
    

    Example usage:

     [DontWrapResult]
     public void TestException()
     {
         throw new HttpStatusCodeException(404, L("ObjectNotFound"));
     }
    

    The custom exception you create here inherits from the UserFriendlyException class. If you want, you can customize it by inheriting from different exception types according to your scenario.

    Another issue here is that the error is wrapped in AjaxResponse Abp ErrorInfo. If you want to convert this, you can use the following example approach.

    public class MyCustomExceptionToErrorInfoConverter : DefaultExceptionToErrorInfoConverter
    {
        public override ErrorInfo Convert(Exception exception, bool includeSensitiveDetails)
        {
            if (exception is HttpStatusCodeException httpEx)
            {
                return new ErrorInfo(httpEx.Code, httpEx.Message);
            }
    
            return base.Convert(exception, includeSensitiveDetails);
        }
    }
    

    And register it in your module:

    Configuration.ReplaceService<IExceptionToErrorInfoConverter, MyCustomExceptionToErrorInfoConverter>();
    
    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    kylem created

    Thanks for the answer.

    That solution still doesn't solve my problem.

    If I apply it, I can correctly handle HttpStatusCodeException, which is not bad. However any other exception thrown by my method won't be wrapped, which is not what I want.

    In addition, solution where I have to annotate most of my method with [DontWrapResult] is not a good solution.

    Just to clarify, I want my results to be wrapped, I just need to tweak wrapping logic a bit so that in certain scenarios instead of generic 500, we return specific http code to the client, and I am hope there is simple way to achieve this.

    I can see from the exception stack trace that exceptions are being handled by AbpExceptionFilter. I tried customizing it to do what I want, but I don't see a way how to register customized version to be used with my application

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @kylem

    By implementing the CustomAbpExceptionFilter, you can handle exceptions more effectively in your application, allowing you to return specific HTTP status codes and detailed messages based on the type of exception. For example, using a custom exception like HttpStatusCodeException, you can control which status code to return (such as 429 for "Too Many Requests") along with a clear message. You may not create an exception here if you wish. If you are not going to use it, you should also remove the check from the filter.

    public class CustomAbpExceptionFilter : AbpExceptionFilter
    {
        private readonly IErrorInfoBuilder _errorInfoBuilder;
        private readonly IAbpWebCommonModuleConfiguration _abpWebCommonModuleConfiguration;
        public CustomAbpExceptionFilter(
            IErrorInfoBuilder errorInfoBuilder,
            IAbpAspNetCoreConfiguration configuration,
            IAbpWebCommonModuleConfiguration abpWebCommonModuleConfiguration)
            : base(errorInfoBuilder, configuration, abpWebCommonModuleConfiguration)
        {
            _errorInfoBuilder = errorInfoBuilder;
            _abpWebCommonModuleConfiguration = abpWebCommonModuleConfiguration;
        }
    
        protected override int GetStatusCode(ExceptionContext context, bool wrapOnError)
        {
            var ex = context.Exception;
    
            if (ex is HttpStatusCodeException httpStatusCodeEx)
            {
                return httpStatusCodeEx.StatusCode;
            }
    
            return base.GetStatusCode(context, wrapOnError);
        }
    
        protected override void HandleAndWrapException(ExceptionContext context, WrapResultAttribute wrapResultAttribute)
        {
            if (!ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType))
            {
                return;
            }
    
            var displayUrl = context.HttpContext.Request.GetDisplayUrl();
            if (_abpWebCommonModuleConfiguration.WrapResultFilters.HasFilterForWrapOnError(displayUrl,
                out var wrapOnError))
            {
                context.HttpContext.Response.StatusCode = GetStatusCode(context, wrapOnError);
    
                if (!wrapOnError)
                {
                    return;
                }
    
                HandleError(context);
                return;
            }
    
            context.HttpContext.Response.StatusCode = GetStatusCode(context, wrapResultAttribute.WrapOnError);
    
            if (!wrapResultAttribute.WrapOnError)
            {
                return;
            }
    
            HandleError(context);
        }
    
        private void HandleError(ExceptionContext context)
        {
            var statusCode = GetStatusCode(context, true);
            var exception = context.Exception;
    
            var errorInfo = _errorInfoBuilder.BuildForException(exception);
    
            if (exception is HttpStatusCodeException httpEx)
            {
                errorInfo.Message = httpEx.Message;
            }
    
            context.Result = new ObjectResult(
                new AjaxResponse(
                    errorInfo,
                    exception is AbpAuthorizationException
                )
            )
            {
                StatusCode = statusCode
            };
    
            context.HttpContext.Response.StatusCode = statusCode;
    
            EventBus.Trigger(this, new AbpHandledExceptionData(exception));
            context.Exception = null;
        }
    }
    
    public class HttpStatusCodeException : Exception
    {
        public int StatusCode { get; }
    
        public HttpStatusCodeException(int statusCode, string message)
            : base(message)
        {
            StatusCode = statusCode;
        }
    
        public HttpStatusCodeException(int statusCode, string message, Exception innerException)
            : base(message, innerException)
        {
            StatusCode = statusCode;
        }
    }
    

    By registering this filter in your YourProjectNameWebMvcModule or YourProjectNameWebHostModule, you ensure that it is applied throughout your application, overriding the default error handling mechanism.

    public class YourProjectNameWebMvcModule : AbpModule
    {
         public override void PreInitialize()
         {
             IocManager.Register<AbpExceptionFilter, CustomAbpExceptionFilter>(DependencyLifeStyle.Transient);
             //...
         }
    }
    

    When you throw an HttpStatusCodeException exception in your code, it is caught by the custom filter and then the appropriate HTTP status and message are returned to the client. If you do not want to override the message, you can remove the message synchronization in the HandleError method.

    throw new HttpStatusCodeException(429, "Too many requests. Please slow down.");
    

    I hope this solution helps with your issue. If you need further information, please feel free to reach out.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    kylem created

    First few tests look good and exactly what I was looking for.Thanks!

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @kylem

    We are glad to have been of assistance. Please do not hesitate to contact us for further information or support.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)