Returning exceptions as JSON messages
Asked Answered
S

3

13

I am developing an API with ASP.NET Core and I am struggling with the exception handling.

When any exception occurs, or in any controller where I want to return custom errors with different status codes, I want to return JSON-formatted exception reports. I do not need HTML in the error response.

I'm not sure if I should use middleware for this, or something else. How should I return JSON exceptions in an ASP.NET Core API?

Stink answered 29/7, 2016 at 17:15 Comment(4)
Sorry but "do not work" is a nonsense. Check the How to create a Minimal, Complete, and Verifiable example, please.Intercellular
Sorry, wrote the question while trainriding. Will elaborate more on it when I am back at office.Stink
@Stink I thought the question had potential for a good canonical answer, so I took the liberty of editing a little and proposing a solution. Feel free to roll back or edit further if you feel I changed the meaning of your question too much.Furey
Possible duplicate of Error handling in ASP.NET Core 1.0 Web API (Sending ex.Message to the client)Defense
S
7

Ok, I got a working solution, that I am pretty happy with.

  1. Add middleware: In the Configure Method, register the middleware (comes with ASP.NET Core).

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        // logging stuff, etc.
    
        app.UseStatusCodePagesWithReExecute("/error/{0}");
        app.UseExceptionHandler("/error");
    
        app.UseMvc(); // if you are using Mvc
    
        // probably other middleware stuff 
    }
    
  2. Create a Class for Messages Write a simple class that represents instances of JSON Error Messages you want to send as a request in any error case:

    public class ExceptionMessageContent
    {
    
        public string Error { get; set; }
        public string Message { get; set; }
    
    }
    
  3. Create Error Controller add the Error Controller that handles all expected and unexpected errors. Note, that these routes correspond to the middleware configuration.

    [Route("[controller]")]
    public class ErrorController : Controller
    {
    
        [HttpGet]
        [Route("")]
        public IActionResult ServerError()
        {
    
            var feature = this.HttpContext.Features.Get<IExceptionHandlerFeature>();
            var content = new ExceptionMessageContent()
            {
                Error = "Unexpected Server Error",
                Message = feature?.Error.Message
            };
            return Content( JsonConvert.SerializeObject( content ), "application/json" );
    
        }
    
    
        [HttpGet]
        [Route("{statusCode}")]
        public IActionResult StatusCodeError(int statusCode)
        {
    
            var feature = this.HttpContext.Features.Get<IExceptionHandlerFeature>();
            var content = new ExceptionMessageContent() { Error = "Server Error", Message = $"The Server responded with status code {statusCode}" };
            return Content( JsonConvert.SerializeObject( content ), "application/json" );
    
        }
    }
    

Now, when I want to throw an error anywhere, I can just do that. The request gets redirected to the error handler and sends a 500 with a nice formatted error message. Also, 404 and other codes are handled gracefully. Any custom status codes I want to send, I can also return them with an instance of my ExceptionMessageContent, for example:

// inside controller, returning IActionResult

var content = new ExceptionMessageContent() { 
    Error = "Bad Request", 
    Message = "Details of why this request is bad." 
};

return BadRequest( content );
Stink answered 1/8, 2016 at 10:12 Comment(0)
F
13

An exception filter (either as an attribute, or a global filter) is what you are looking for. From the docs:

Exception filters handle unhandled exceptions, including those that occur during controller creation and model binding. They are only called when an exception occurs in the pipeline. They can provide a single location to implement common error handling policies within an app.

If you want any unhandled exception to be returned as JSON, this is the simplest method:

public class JsonExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        var result = new ObjectResult(new
        {
            code = 500,
            message = "A server error occurred.",
            detailedMessage = context.Exception.Message
        });

        result.StatusCode = 500;
        context.Result = result;
    }
}

You can customize the response to add as much detail as you want. The ObjectResult will be serialized to JSON.

Add the filter as a global filter for MVC in Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Filters.Add(typeof(JsonExceptionFilter));
    });
}
Furey answered 29/7, 2016 at 17:50 Comment(2)
This handles all my uncatched failures, which is a good start. Thx. But what if I wanted to return a BadRequest or any other status code from a controller programmatically?Stink
note, that Exception filters handle only MVC exceptions, more generic approach in ASP.NET Core is to use ExceptionHandler middleware: stackoverflow.com/documentation/asp.net-core/1479/middleware/…Defense
S
7

Ok, I got a working solution, that I am pretty happy with.

  1. Add middleware: In the Configure Method, register the middleware (comes with ASP.NET Core).

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        // logging stuff, etc.
    
        app.UseStatusCodePagesWithReExecute("/error/{0}");
        app.UseExceptionHandler("/error");
    
        app.UseMvc(); // if you are using Mvc
    
        // probably other middleware stuff 
    }
    
  2. Create a Class for Messages Write a simple class that represents instances of JSON Error Messages you want to send as a request in any error case:

    public class ExceptionMessageContent
    {
    
        public string Error { get; set; }
        public string Message { get; set; }
    
    }
    
  3. Create Error Controller add the Error Controller that handles all expected and unexpected errors. Note, that these routes correspond to the middleware configuration.

    [Route("[controller]")]
    public class ErrorController : Controller
    {
    
        [HttpGet]
        [Route("")]
        public IActionResult ServerError()
        {
    
            var feature = this.HttpContext.Features.Get<IExceptionHandlerFeature>();
            var content = new ExceptionMessageContent()
            {
                Error = "Unexpected Server Error",
                Message = feature?.Error.Message
            };
            return Content( JsonConvert.SerializeObject( content ), "application/json" );
    
        }
    
    
        [HttpGet]
        [Route("{statusCode}")]
        public IActionResult StatusCodeError(int statusCode)
        {
    
            var feature = this.HttpContext.Features.Get<IExceptionHandlerFeature>();
            var content = new ExceptionMessageContent() { Error = "Server Error", Message = $"The Server responded with status code {statusCode}" };
            return Content( JsonConvert.SerializeObject( content ), "application/json" );
    
        }
    }
    

Now, when I want to throw an error anywhere, I can just do that. The request gets redirected to the error handler and sends a 500 with a nice formatted error message. Also, 404 and other codes are handled gracefully. Any custom status codes I want to send, I can also return them with an instance of my ExceptionMessageContent, for example:

// inside controller, returning IActionResult

var content = new ExceptionMessageContent() { 
    Error = "Bad Request", 
    Message = "Details of why this request is bad." 
};

return BadRequest( content );
Stink answered 1/8, 2016 at 10:12 Comment(0)
B
0

I'd suggest using ASP.NET Middleware with ProblemDetails. You could use an ExceptionFilter as was also suggested but that is more of an "old-school" pattern - though either work.

This solution uses both ProblemDetails see here and the RFC here as well as Ben Adams' Exception Demystifier nuget

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IWebHostEnvironment _hostingEnvironment;

    public CustomExceptionMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnvironment)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
        _hostingEnvironment = hostingEnvironment;
    }

    [SuppressMessage("Design", "CA1062:Validate arguments of public methods",
        Justification = "This might result in a stack overflow if we throw an exception in middleware designed to handle exceptions")]
    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex);

            // Don't log it here - we have other middleware that handles that - see CustomLoggingMiddleware
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    [SuppressMessage("ReSharper", "PossibleNullReferenceException", Justification = "params will never be null")]
    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        if (exception == null) throw new ArgumentNullException(nameof(exception));
#if DEBUG
        // The middleware shouldn't ever be called thru a HostedService - e.g. Hangfire scheduled jobs, but just in case....
        Debug.Assert(context != null, "HttpContext is null - is this error from a Hangfire job?");
#else
        if (context == null) return Task.CompletedTask;
#endif
        var errorStatusCode = exception is UnauthorizedAccessException ||
                                exception is SecurityTokenException ?
            StatusCodes.Status401Unauthorized : StatusCodes.Status500InternalServerError;

        context.Response.ContentType = "application/problem+json";
        context.Response.StatusCode = errorStatusCode;

        var instanceMessage = $"{CorrelationIdOptions.DefaultHeader}: {context.GetCorrelationId()}";

        //don't show full details unless we're running LOCAL environment!
        string errorDetail;

        if (_hostingEnvironment.IsLocal())
        {
            // ApiException's ToString override calls ToStringDemystified(), so call the ApiException ToString() override
            errorDetail = exception is ApiException ? exception.ToString() : exception.ToStringDemystified();
        }
        else
        {
            errorDetail = "The instance value should be used to identify the problem when contacting customer support";
        }

        var errorDetailsJson = new ProblemDetails
        {
            Detail = errorDetail,
            Instance = instanceMessage,
            Status = errorStatusCode,
            Title = Constants.UnhandledExceptionTitle
        };

        return context.Response.WriteAsync(JsonSerializer.Serialize(errorDetailsJson));
    }
}

Buseck answered 13/12, 2022 at 13:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.