mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
refactor: add Result<T> pattern and global exception handler for consistent error handling
This commit is contained in:
88
octo-fiesta/Middleware/GlobalExceptionHandler.cs
Normal file
88
octo-fiesta/Middleware/GlobalExceptionHandler.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
|
|
||||||
|
namespace octo_fiesta.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Global exception handler that catches unhandled exceptions and returns appropriate Subsonic API error responses
|
||||||
|
/// </summary>
|
||||||
|
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, "Unhandled exception occurred: {Message}", exception.Message);
|
||||||
|
|
||||||
|
var (statusCode, subsonicErrorCode, errorMessage) = MapExceptionToResponse(exception);
|
||||||
|
|
||||||
|
httpContext.Response.StatusCode = statusCode;
|
||||||
|
httpContext.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
var response = CreateSubsonicErrorResponse(subsonicErrorCode, errorMessage);
|
||||||
|
await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps exception types to HTTP status codes and Subsonic error codes
|
||||||
|
/// </summary>
|
||||||
|
private (int statusCode, int subsonicErrorCode, string message) MapExceptionToResponse(Exception exception)
|
||||||
|
{
|
||||||
|
return exception switch
|
||||||
|
{
|
||||||
|
// Not Found errors (404)
|
||||||
|
FileNotFoundException => (404, 70, "Resource not found"),
|
||||||
|
DirectoryNotFoundException => (404, 70, "Directory not found"),
|
||||||
|
|
||||||
|
// Authentication errors (401)
|
||||||
|
UnauthorizedAccessException => (401, 40, "Wrong username or password"),
|
||||||
|
|
||||||
|
// Bad Request errors (400)
|
||||||
|
ArgumentNullException => (400, 10, "Required parameter is missing"),
|
||||||
|
ArgumentException => (400, 10, "Invalid request"),
|
||||||
|
FormatException => (400, 10, "Invalid format"),
|
||||||
|
InvalidOperationException => (400, 10, "Operation not valid"),
|
||||||
|
|
||||||
|
// External service errors (502)
|
||||||
|
HttpRequestException => (502, 0, "External service unavailable"),
|
||||||
|
TimeoutException => (504, 0, "Request timeout"),
|
||||||
|
|
||||||
|
// Generic server error (500)
|
||||||
|
_ => (500, 0, "An internal server error occurred")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Subsonic-compatible error response
|
||||||
|
/// Subsonic error codes:
|
||||||
|
/// 0 = Generic error
|
||||||
|
/// 10 = Required parameter missing
|
||||||
|
/// 20 = Incompatible Subsonic REST protocol version
|
||||||
|
/// 30 = Incompatible Subsonic REST protocol version (server)
|
||||||
|
/// 40 = Wrong username or password
|
||||||
|
/// 50 = User not authorized
|
||||||
|
/// 60 = Trial period for the Subsonic server is over
|
||||||
|
/// 70 = Requested data was not found
|
||||||
|
/// </summary>
|
||||||
|
private object CreateSubsonicErrorResponse(int code, string message)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["subsonic-response"] = new
|
||||||
|
{
|
||||||
|
status = "failed",
|
||||||
|
version = "1.16.1",
|
||||||
|
error = new { code, message }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using octo_fiesta.Services.Deezer;
|
|||||||
using octo_fiesta.Services.Qobuz;
|
using octo_fiesta.Services.Qobuz;
|
||||||
using octo_fiesta.Services.Local;
|
using octo_fiesta.Services.Local;
|
||||||
using octo_fiesta.Services.Validation;
|
using octo_fiesta.Services.Validation;
|
||||||
|
using octo_fiesta.Middleware;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -14,6 +15,10 @@ builder.Services.AddHttpClient();
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
// Exception handling
|
||||||
|
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
|
||||||
|
builder.Services.AddProblemDetails();
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
builder.Services.Configure<SubsonicSettings>(
|
builder.Services.Configure<SubsonicSettings>(
|
||||||
builder.Configuration.GetSection("Subsonic"));
|
builder.Configuration.GetSection("Subsonic"));
|
||||||
@@ -66,6 +71,8 @@ builder.Services.AddCors(options =>
|
|||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
app.UseExceptionHandler(_ => { }); // Global exception handler
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
|
|||||||
140
octo-fiesta/Services/Common/Error.cs
Normal file
140
octo-fiesta/Services/Common/Error.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
namespace octo_fiesta.Services.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a typed error with code, message, and metadata
|
||||||
|
/// </summary>
|
||||||
|
public class Error
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique error code identifier
|
||||||
|
/// </summary>
|
||||||
|
public string Code { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Human-readable error message
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error type/category
|
||||||
|
/// </summary>
|
||||||
|
public ErrorType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional metadata about the error
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object>? Metadata { get; }
|
||||||
|
|
||||||
|
private Error(string code, string message, ErrorType type, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
Message = message;
|
||||||
|
Type = type;
|
||||||
|
Metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Not Found error (404)
|
||||||
|
/// </summary>
|
||||||
|
public static Error NotFound(string message, string? code = null, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code ?? "NOT_FOUND", message, ErrorType.NotFound, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Validation error (400)
|
||||||
|
/// </summary>
|
||||||
|
public static Error Validation(string message, string? code = null, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code ?? "VALIDATION_ERROR", message, ErrorType.Validation, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an Unauthorized error (401)
|
||||||
|
/// </summary>
|
||||||
|
public static Error Unauthorized(string message, string? code = null, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code ?? "UNAUTHORIZED", message, ErrorType.Unauthorized, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Forbidden error (403)
|
||||||
|
/// </summary>
|
||||||
|
public static Error Forbidden(string message, string? code = null, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code ?? "FORBIDDEN", message, ErrorType.Forbidden, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Conflict error (409)
|
||||||
|
/// </summary>
|
||||||
|
public static Error Conflict(string message, string? code = null, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code ?? "CONFLICT", message, ErrorType.Conflict, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an Internal Server Error (500)
|
||||||
|
/// </summary>
|
||||||
|
public static Error Internal(string message, string? code = null, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code ?? "INTERNAL_ERROR", message, ErrorType.Internal, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an External Service Error (502/503)
|
||||||
|
/// </summary>
|
||||||
|
public static Error ExternalService(string message, string? code = null, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code ?? "EXTERNAL_SERVICE_ERROR", message, ErrorType.ExternalService, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a custom error with specified type
|
||||||
|
/// </summary>
|
||||||
|
public static Error Custom(string code, string message, ErrorType type, Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
return new Error(code, message, type, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Categorizes error types for appropriate HTTP status code mapping
|
||||||
|
/// </summary>
|
||||||
|
public enum ErrorType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validation error (400 Bad Request)
|
||||||
|
/// </summary>
|
||||||
|
Validation,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resource not found (404 Not Found)
|
||||||
|
/// </summary>
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication required (401 Unauthorized)
|
||||||
|
/// </summary>
|
||||||
|
Unauthorized,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Insufficient permissions (403 Forbidden)
|
||||||
|
/// </summary>
|
||||||
|
Forbidden,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resource conflict (409 Conflict)
|
||||||
|
/// </summary>
|
||||||
|
Conflict,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal server error (500 Internal Server Error)
|
||||||
|
/// </summary>
|
||||||
|
Internal,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// External service error (502 Bad Gateway / 503 Service Unavailable)
|
||||||
|
/// </summary>
|
||||||
|
ExternalService
|
||||||
|
}
|
||||||
99
octo-fiesta/Services/Common/Result.cs
Normal file
99
octo-fiesta/Services/Common/Result.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
namespace octo_fiesta.Services.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of an operation that can either succeed with a value or fail with an error.
|
||||||
|
/// This pattern allows explicit error handling without using exceptions for control flow.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the value returned on success</typeparam>
|
||||||
|
public class Result<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the operation succeeded
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the operation failed
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFailure => !IsSuccess;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value returned on success (null if failed)
|
||||||
|
/// </summary>
|
||||||
|
public T? Value { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The error that occurred on failure (null if succeeded)
|
||||||
|
/// </summary>
|
||||||
|
public Error? Error { get; }
|
||||||
|
|
||||||
|
private Result(bool isSuccess, T? value, Error? error)
|
||||||
|
{
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
Value = value;
|
||||||
|
Error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a successful result with a value
|
||||||
|
/// </summary>
|
||||||
|
public static Result<T> Success(T value)
|
||||||
|
{
|
||||||
|
return new Result<T>(true, value, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a failed result with an error
|
||||||
|
/// </summary>
|
||||||
|
public static Result<T> Failure(Error error)
|
||||||
|
{
|
||||||
|
return new Result<T>(false, default, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implicit conversion from T to Result<T> for convenience
|
||||||
|
/// </summary>
|
||||||
|
public static implicit operator Result<T>(T value)
|
||||||
|
{
|
||||||
|
return Success(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implicit conversion from Error to Result<T> for convenience
|
||||||
|
/// </summary>
|
||||||
|
public static implicit operator Result<T>(Error error)
|
||||||
|
{
|
||||||
|
return Failure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Non-generic Result for operations that don't return a value
|
||||||
|
/// </summary>
|
||||||
|
public class Result
|
||||||
|
{
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
public bool IsFailure => !IsSuccess;
|
||||||
|
public Error? Error { get; }
|
||||||
|
|
||||||
|
private Result(bool isSuccess, Error? error)
|
||||||
|
{
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
Error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result Success()
|
||||||
|
{
|
||||||
|
return new Result(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result Failure(Error error)
|
||||||
|
{
|
||||||
|
return new Result(false, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator Result(Error error)
|
||||||
|
{
|
||||||
|
return Failure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user