using Microsoft.AspNetCore.Mvc; using Tiku.Api.Controllers; using Tiku.Application.Assets; using Tiku.Application.Auth; using Tiku.Application.Storage; using Tiku.Infrastructure.Content; using Tiku.Infrastructure.Learning; using Tiku.Infrastructure.QuestionBanks; namespace Tiku.Api.Middleware; public sealed class ExceptionHandlingMiddleware( RequestDelegate next, ILogger logger, IHostEnvironment environment) { public async Task InvokeAsync(HttpContext context) { try { await next(context); } catch (Exception exception) { if (exception is AuthException authException) { await WriteAuthProblemAsync(context, authException); return; } if (exception is TenantNotFoundException) { await WriteProblemAsync( context, "Tenant was not found.", StatusCodes.Status404NotFound, "tenant_not_found"); return; } if (exception is RequiredFieldException) { await WriteProblemAsync( context, exception.Message, StatusCodes.Status400BadRequest, "required_field"); return; } if (exception is ContentNavigationNotFoundException) { await WriteProblemAsync( context, exception.Message, StatusCodes.Status404NotFound, "content_navigation_not_found"); return; } if (exception is QuestionBankRequiredFieldException) { await WriteProblemAsync( context, exception.Message, StatusCodes.Status400BadRequest, "required_field"); return; } if (exception is QuestionBankNotFoundException) { await WriteProblemAsync( context, exception.Message, StatusCodes.Status404NotFound, "question_not_found"); return; } if (exception is AssetAccessException assetAccessException) { await WriteProblemAsync( context, assetAccessException.Message, AssetAccessStatusCode(assetAccessException.Code), assetAccessException.Code.ToLowerInvariant()); return; } if (exception is AssetManagementException assetManagementException) { await WriteProblemAsync( context, assetManagementException.Message, AssetManagementStatusCode(assetManagementException.Code), assetManagementException.Code); return; } if (exception is LearningValidationException learningValidationException) { await WriteProblemAsync( context, learningValidationException.Message, LearningValidationStatusCode(learningValidationException.Code), learningValidationException.Code); return; } if (exception is LearningResourceNotFoundException learningResourceNotFoundException) { await WriteProblemAsync( context, learningResourceNotFoundException.Message, StatusCodes.Status404NotFound, learningResourceNotFoundException.Code); return; } if (exception is LearningAccessDeniedException) { await WriteProblemAsync( context, exception.Message, StatusCodes.Status403Forbidden, "learning_access_denied"); return; } if (exception is ObjectStorageException storageException) { await WriteProblemAsync( context, storageException.Message, storageException is ObjectStorageNotConfiguredException ? StatusCodes.Status503ServiceUnavailable : StatusCodes.Status400BadRequest, storageException.Code.ToLowerInvariant()); return; } logger.LogError(exception, "Unhandled API exception"); var problem = new ProblemDetails { Title = "An unexpected error occurred.", Status = StatusCodes.Status500InternalServerError, Detail = environment.IsDevelopment() ? exception.Message : null, Instance = context.Request.Path }; problem.Extensions["traceId"] = context.TraceIdentifier; context.Response.StatusCode = StatusCodes.Status500InternalServerError; await context.Response.WriteAsJsonAsync(problem); } } private static async Task WriteProblemAsync( HttpContext context, string title, int status, string code) { var problem = new ProblemDetails { Title = title, Status = status, Instance = context.Request.Path }; problem.Extensions["code"] = code; problem.Extensions["traceId"] = context.TraceIdentifier; context.Response.StatusCode = status; await context.Response.WriteAsJsonAsync(problem); } private static async Task WriteAuthProblemAsync(HttpContext context, AuthException exception) { var status = exception.Code switch { "tenant_access_denied" => StatusCodes.Status403Forbidden, "sms_rate_limited" => StatusCodes.Status429TooManyRequests, "auth_provider_not_configured" => StatusCodes.Status503ServiceUnavailable, "session_revoked" => StatusCodes.Status401Unauthorized, _ => StatusCodes.Status401Unauthorized }; var problem = new ProblemDetails { Title = exception.Message, Status = status, Instance = context.Request.Path }; problem.Extensions["code"] = exception.Code; problem.Extensions["traceId"] = context.TraceIdentifier; context.Response.StatusCode = status; await context.Response.WriteAsJsonAsync(problem); } private static int AssetAccessStatusCode(string code) { return code switch { "ASSET_NOT_FOUND" => StatusCodes.Status404NotFound, "AUTH_REQUIRED" => StatusCodes.Status401Unauthorized, "ASSET_HIDDEN" or "ASSET_MEMBERSHIP_REQUIRED" or "ASSET_SVIP_REQUIRED" => StatusCodes.Status403Forbidden, "ASSET_UPLOAD_NOT_VERIFIED" or "ASSET_SECURITY_SCAN_NOT_PASSED" => StatusCodes.Status409Conflict, "ASSET_PREVIEW_NOT_SUPPORTED" => StatusCodes.Status400BadRequest, _ => StatusCodes.Status400BadRequest }; } private static int LearningValidationStatusCode(string code) { return code switch { "no_practice_questions" or "practice_session_empty" => StatusCodes.Status409Conflict, _ => StatusCodes.Status400BadRequest }; } private static int AssetManagementStatusCode(string code) { return code switch { "asset_not_found" or "import_job_not_found" => StatusCodes.Status404NotFound, "asset_upload_missing" => StatusCodes.Status409Conflict, "tenant_content_access_denied" => StatusCodes.Status403Forbidden, _ => StatusCodes.Status400BadRequest }; } }