feat(api,infra): implement GlobalExceptionHandler and enhance PlaidException for detailed error handling

This commit is contained in:
Stevan Freeborn
2026-03-11 18:52:23 -05:00
parent 64c64e8d95
commit a721c60ad9
4 changed files with 101 additions and 16 deletions
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Diagnostics;
internal sealed class GlobalExceptionHandler(
IProblemDetailsService problemDetailsService,
ILogger<GlobalExceptionHandler> logger
) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken
)
{
logger.LogError(exception, "An unhandled exception occurred");
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
HttpContext = httpContext,
Exception = exception,
ProblemDetails = new ProblemDetails
{
Type = exception.GetType().Name,
Title = "Uh oh something has gone wrong.",
Detail = exception.Message
}
});
}
}
+1
View File
@@ -3,6 +3,7 @@ var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddValidation();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
builder.Services.ConfigureHttpJsonOptions(static options =>
@@ -31,7 +31,7 @@ internal sealed class PlaidAccountService : IPlaidAccountService
var assemblyName = Assembly.GetExecutingAssembly().GetName().FullName;
var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var ltr = await _client.LinkTokenCreateAsync(new()
var linkTokenResponse = await _client.LinkTokenCreateAsync(new()
{
ClientName = $"{assemblyName}_{environmentName}",
Products = [Products.Transactions],
@@ -44,56 +44,73 @@ internal sealed class PlaidAccountService : IPlaidAccountService
Webhook = _options.Value.Webhook,
}).ConfigureAwait(false);
if (ltr.IsSuccessStatusCode is false)
if (linkTokenResponse.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to create link token");
throw new PlaidException(
"Unable to create link token",
linkTokenResponse.Error,
linkTokenResponse.RequestId,
(int?)linkTokenResponse.StatusCode
);
}
return ltr.LinkToken;
return linkTokenResponse.LinkToken;
}
public async Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken)
{
var ptr = await _client.ItemPublicTokenExchangeAsync(new()
var publicTokenResponse = await _client.ItemPublicTokenExchangeAsync(new()
{
PublicToken = publicToken,
}).ConfigureAwait(false);
if (ptr.IsSuccessStatusCode is false)
if (publicTokenResponse.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to exchange public token for access token");
throw new PlaidException(
"Unable to exchange public token for access token",
publicTokenResponse.Error,
publicTokenResponse.RequestId,
(int?)publicTokenResponse.StatusCode
);
}
return (ptr.ItemId, ptr.AccessToken);
return (publicTokenResponse.ItemId, publicTokenResponse.AccessToken);
}
public async Task<List<Going.Plaid.Entity.Account>> GetAccountsAsync(string accessToken)
{
var ar = await _client.AccountsGetAsync(new()
var accountResponse = await _client.AccountsGetAsync(new()
{
AccessToken = accessToken,
}).ConfigureAwait(false);
if (ar.IsSuccessStatusCode is false)
if (accountResponse.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to retrieve accounts");
throw new PlaidException(
"Unable to retrieve accounts",
accountResponse.Error,
accountResponse.RequestId,
(int?)accountResponse.StatusCode
);
}
return [.. ar.Accounts];
return [.. accountResponse.Accounts];
}
public async Task<ItemWithConsentFields> GetItemAsync(string accessToken)
{
var ar = await _client.ItemGetAsync(new()
var itemResponse = await _client.ItemGetAsync(new()
{
AccessToken = accessToken,
}).ConfigureAwait(false);
if (ar.IsSuccessStatusCode is false)
if (itemResponse.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to retrieve item");
throw new PlaidException(
"Unable to retrieve item"
);
}
return ar.Item;
return itemResponse.Item;
}
}
@@ -2,6 +2,13 @@ namespace FiscalOS.Infra.Common;
public class PlaidException : Exception
{
public PlaidError? Error { get; }
public string? RequestId { get; }
public int? StatusCode { get; }
public string? ErrorCode => Error?.ErrorCode;
public string? ErrorType => Error?.ErrorType;
public string? ErrorMessage => Error?.ErrorMessage;
public PlaidException()
{
}
@@ -13,4 +20,34 @@ public class PlaidException : Exception
public PlaidException(string message, Exception innerException) : base(message, innerException)
{
}
public PlaidException(
string message,
PlaidError? error = null,
string? plaidRequestId = null,
int? statusCode = null,
Exception? innerException = null
) : base(message, innerException)
{
Error = error;
RequestId = plaidRequestId;
StatusCode = statusCode;
if (error is not null)
{
Data[nameof(ErrorCode)] = error.ErrorCode;
Data[nameof(ErrorType)] = error.ErrorType;
Data[nameof(ErrorMessage)] = error.ErrorMessage;
}
if (plaidRequestId is not null)
{
Data[nameof(RequestId)] = plaidRequestId;
}
if (statusCode is not null)
{
Data[nameof(StatusCode)] = statusCode;
}
}
}