Commit: bf804da
Parent: 5d543c2

Use built-in `ProblemDetails` and exception handler

Mårten Åsberg committed on 2026-02-21 at 19:01
IMPROVEMENTS.md +8 -0
diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md
index 0e27ed8..766ddad 100644
@@ -11,3 +11,11 @@ Document all response types and status codes in the OpenAPI specification.
Use an injected `TimeProvider`. This will help reduce flakey unit tests as the tests no longer rely on actual wall time.
Non of the `OrderServiceTests` are very interesting, because there's no interesting logic in the `OrderService`.
Consider moving the test project to `/test`. If not I'd suggest to simply remove `/src` and have all projects in the root directory.
### Task 4: Considerations
Using the built-in `ProblemDetails` instead of a custom exception format for a unified response format on all kinds of errors.
My philosophy is that any exception thrown should be truly exceptional, and therefore all exceptions should be returned as 500 errors. In case of an expected error happening in the service (or lower) it should be visible in the return type, and the controller should map it to an appropriate response. Right now the only expected "error" is not finding an order, so returning a nullable order is fine, but if other expected errors (such as unauthorized access maybe) is added to the service logic then the return type has to change.
src/Walley.Checkout.Api/Middleware/ExceptionMiddleware.cs +0 -41
diff --git a/src/Walley.Checkout.Api/Middleware/ExceptionMiddleware.cs b/src/Walley.Checkout.Api/Middleware/ExceptionMiddleware.cs
deleted file mode 100644
index fd792ea..0000000
@@ -1,41 +0,0 @@
using System.Net;
using System.Text.Json;
using Walley.Checkout.Api.Models;
namespace Walley.Checkout.Api.Middleware;
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var response = new ApiErrorResponse
{
Message = "An unexpected error occurred.",
StatusCode = context.Response.StatusCode
};
var json = JsonSerializer.Serialize(response);
await context.Response.WriteAsync(json);
}
}
}
src/Walley.Checkout.Api/Models/ApiErrorResponse.cs +0 -8
diff --git a/src/Walley.Checkout.Api/Models/ApiErrorResponse.cs b/src/Walley.Checkout.Api/Models/ApiErrorResponse.cs
deleted file mode 100644
index 513764e..0000000
@@ -1,8 +0,0 @@
namespace Walley.Checkout.Api.Models;
public class ApiErrorResponse
{
public string Message { get; set; } = string.Empty;
public string? Detail { get; set; }
public int StatusCode { get; set; }
}
src/Walley.Checkout.Api/Program.cs +2 -2
diff --git a/src/Walley.Checkout.Api/Program.cs b/src/Walley.Checkout.Api/Program.cs
index 907049b..7bfca7b 100644
@@ -1,9 +1,9 @@
using Walley.Checkout.Api.Middleware;
using Walley.Checkout.Api.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
@@ -20,7 +20,7 @@ builder.Services.AddScoped<IRefundService, RefundService>();
var app = builder.Build();
app.UseMiddleware<ExceptionMiddleware>();
app.UseExceptionHandler();
if (app.Environment.IsDevelopment())
{