ASP.NET Core Custom IExceptionHandler doesn't catch exceptions

iKingNinja 120 Reputation points
2024-08-29T09:40:46.8266667+00:00

I'm trying to create a global exception handler for my Web API using IExceptionHandler. However my handler is not catching any exception.


public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler

{       

    public ValueTask<bool> TryHandleAsync(HttpContext context, Exception ex, CancellationToken ct)

    {

        logger.LogError(ex, "Controller error occured");

        ErrorResponse errors = new()

        {

            Errors = [new ApiError(-1, "Internal Server Error")]

        };

        string jsonRes = JsonSerializer.Serialize(errors);

        context.Response.BodyWriter.Write(Encoding.UTF8.GetBytes(jsonRes));

        return ValueTask.FromResult(true);

    }

}

Program.cs


builder.Services.AddExceptionHandler<GlobalExceptionHandler>();


using Asp.Versioning;

using API.Services;

using NRediSearch;

using StackExchange.Redis;

using API.Authentication.ApiKeyAuthenticaiton;

using Microsoft.OpenApi.Models;

using API;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddEnvironmentVariables();

IConfiguration configuration = builder.Configuration;

// Add services to the container.

builder.Services.AddControllers();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddApiVersioning(options =>

{

    options.DefaultApiVersion = new ApiVersion(1);

    options.ReportApiVersions = true;

    options.AssumeDefaultVersionWhenUnspecified = false;

    options.ApiVersionReader = ApiVersionReader.Combine(

        new UrlSegmentApiVersionReader());

}).AddApiExplorer(options =>

{

    options.GroupNameFormat = "'v'V";

    options.SubstituteApiVersionInUrl = true;

});

builder.Services.AddSwaggerGen(options =>

{

    options.EnableAnnotations();

    options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo

    {

        Title = "API",

        Version = "v1"

    });

    options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.AuthenticationScheme, new()

    {

        Name = "x-api-key",

        Description = "API key authorization",

        In = ParameterLocation.Header,

        Type = SecuritySchemeType.ApiKey

    });

    options.AddSecurityRequirement(new()

    {

        {

            new OpenApiSecurityScheme()

            {

                Reference = new OpenApiReference()

                {

                    Type = ReferenceType.SecurityScheme,

                    Id = ApiKeyAuthenticationDefaults.AuthenticationScheme

                },

                Name = ApiKeyAuthenticationDefaults.AuthenticationScheme,

                In = ParameterLocation.Header

            },

            new List<string>()

        }

    });

});

builder.Services.Configure<RouteOptions>(options =>

{

    options.LowercaseUrls = true;

});

string redisConnectionString = builder.Configuration.GetConnectionString("Redis") ?? throw new Exception("No Redis connection string found");

builder.Services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(redisConnectionString));

builder.Services.AddSingleton(serviceProvider =>

{

    IConnectionMultiplexer multiplexer = serviceProvider.GetRequiredService<IConnectionMultiplexer>();

    IDatabase db = multiplexer.GetDatabase();

    Client client = new("queueIdx", db);

    return client;

});

builder.Services.AddSingleton<IRedisService, RedisService>();

builder.Services.AddScoped<IQueueManager, QueueManager>();

builder.Services.AddSingleton<QueuesPoolService>();

builder.Services.AddSingleton(configuration);

builder.Logging.AddConsole();

builder.Services.AddAuthentication(options =>

{

    options.DefaultAuthenticateScheme = ApiKeyAuthenticationDefaults.AuthenticationScheme;

    options.DefaultChallengeScheme = ApiKeyAuthenticationDefaults.AuthenticationScheme;

})

    .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, options =>

    {

        options.ApiKey = configuration["ApiKey"] ?? throw new Exception("No API key was configured");

    });

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

// Configure the HTTP request pipeline.

if (app.Environment.IsDevelopment())

{

    app.UseSwagger();

    app.UseSwaggerUI();

}

app.UseHttpsRedirection();

app.UseAuthentication();

app.UseAuthorization();

// Set up Redis DB

IRedisService redis = app.Services.GetRequiredService<IRedisService>();

await redis.CreateIndexAsync();

await redis.ConfigureAsync();

app.MapControllers();

app.Run();

I am purposefully throwing an error from a controller action and it is not being caught. The debugger breakpoint is not hit at all.


public async Task<IActionResult> GetCurrentPlayerQueue(long userId)

{

    throw new Exception("test");

}

The API follows the default exception handling behavior: it returns the full exceptions details in development environment and an empty response body in production env. I expect my API to use GlobalExceptionHandler and write the errors to the response body.

Developer technologies ASP.NET ASP.NET Core
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Anonymous
    2024-08-29T10:41:38.56+00:00

    Hi @iKingNinja,

    UPDATE

    Method 1 - ExceptionHandler

    GlobalExceptionHandler.cs

    using Microsoft.AspNetCore.Diagnostics;
    using System.Buffers;
    using System.Text.Json;
    using System.Text;
    using Microsoft.AspNetCore.Mvc;
    
    namespace WebApplication1
    {
        public class GlobalExceptionHandler : IExceptionHandler
        {
            private readonly ILogger<GlobalExceptionHandler> _logger;
    
            public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
            {
                _logger = logger;
            }
    
            public async ValueTask<bool> TryHandleAsync(
                HttpContext httpContext,
                Exception exception,
                CancellationToken cancellationToken)
            {
                _logger.LogError(
                    exception, "Exception occurred: {Message}", exception.Message);
    
                var problemDetails = new ProblemDetails
                {
                    Status = StatusCodes.Status500InternalServerError,
                    Title = "Server error"
                };
    
                httpContext.Response.StatusCode = problemDetails.Status.Value;
    
                await httpContext.Response
                    .WriteAsJsonAsync(problemDetails, cancellationToken);
    
                return true;
            }
        }
    }
    

    Register it.

    ...
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
    // Add this Line
    builder.Services.AddProblemDetails();
    
    var app = builder.Build();
    // Add this line
    app.UseExceptionHandler();
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    ...
    

    Test Result

    User's image


    Method 2 - Middleware

    You should use middleware, not IExceptionHandler. Here is the working sample.

    GlobalExceptionHandlerMiddleware.cs

    using System.Text.Json;
    namespace WebApplication1
    {
        public class GlobalExceptionHandlerMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
            public GlobalExceptionHandlerMiddleware(RequestDelegate next, ILogger<GlobalExceptionHandlerMiddleware> logger)
            {
                _next = next;
                _logger = logger;
            }
            public async Task InvokeAsync(HttpContext context)
            {
                try
                {
                    await _next(context);
                }
                catch (Exception ex)
                {
                    await HandleExceptionAsync(context, ex);
                }
            }
            private Task HandleExceptionAsync(HttpContext context, Exception exception)
            {
                _logger.LogError(exception, "An unhandled exception occurred.");
                var errorResponse = new
                {
                    Errors = new[] { new { Code = -1, Message = "Internal Server Error" } }
                };
                var result = JsonSerializer.Serialize(errorResponse);
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = StatusCodes.Status500InternalServerError;
                return context.Response.WriteAsync(result);
            }
        }
    }
    

    Register it.

    using WebApplication1;
    
    var builder = WebApplication.CreateBuilder(args);
    // Add services to the container.
    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    //builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
    
    var app = builder.Build();
    // Tip
    // Register it in the top, it can capture all the exceptions
    app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
    
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    app.UseHttpsRedirection();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
    

    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best regards,

    Jason

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.