skip to Main Content

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.

2

Answers


  1. Chosen as BEST ANSWER

    I found the fix: for controller based Web APIs, you have to create a Controller and add a no-method action to bind UseExceptionHandler() to.

    [ApiController]
    [Route("error")]
    public class ErrorController : Controller
    {
        public IActionResult Index() =>
            Problem();
    }
    
    app.UseExceptionHandler("/error");
    

    Source: https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-8.0#exception-handler


  2. METHOD 1 – ExceptionHandler

    Change your GlobalExceptionHandler like below and use ProblemDetails.

    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;
            }
        }
    }
    

    And Register it like below.

    ...
    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();
    }
    ...
    

    METHOD 2 – Middleware

    We can create a custom middleware to capture Global Exception.

    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);
            }
        }
    }
    

    Program.cs

    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();
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search