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
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.Source: https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-8.0#exception-handler
METHOD 1 – ExceptionHandler
Change your GlobalExceptionHandler like below and use
ProblemDetails
.And Register it like below.
METHOD 2 – Middleware
We can create a custom middleware to capture Global Exception.
GlobalExceptionHandlerMiddleware.cs
Program.cs