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.AddServiceDefaults();
builder.Services.AddValidation(); builder.Services.AddValidation();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails(); builder.Services.AddProblemDetails();
builder.Services.ConfigureHttpJsonOptions(static options => builder.Services.ConfigureHttpJsonOptions(static options =>
@@ -31,7 +31,7 @@ internal sealed class PlaidAccountService : IPlaidAccountService
var assemblyName = Assembly.GetExecutingAssembly().GetName().FullName; var assemblyName = Assembly.GetExecutingAssembly().GetName().FullName;
var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var ltr = await _client.LinkTokenCreateAsync(new() var linkTokenResponse = await _client.LinkTokenCreateAsync(new()
{ {
ClientName = $"{assemblyName}_{environmentName}", ClientName = $"{assemblyName}_{environmentName}",
Products = [Products.Transactions], Products = [Products.Transactions],
@@ -44,56 +44,73 @@ internal sealed class PlaidAccountService : IPlaidAccountService
Webhook = _options.Value.Webhook, Webhook = _options.Value.Webhook,
}).ConfigureAwait(false); }).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) public async Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken)
{ {
var ptr = await _client.ItemPublicTokenExchangeAsync(new() var publicTokenResponse = await _client.ItemPublicTokenExchangeAsync(new()
{ {
PublicToken = publicToken, PublicToken = publicToken,
}).ConfigureAwait(false); }).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) 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, AccessToken = accessToken,
}).ConfigureAwait(false); }).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) public async Task<ItemWithConsentFields> GetItemAsync(string accessToken)
{ {
var ar = await _client.ItemGetAsync(new() var itemResponse = await _client.ItemGetAsync(new()
{ {
AccessToken = accessToken, AccessToken = accessToken,
}).ConfigureAwait(false); }).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 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() public PlaidException()
{ {
} }
@@ -13,4 +20,34 @@ public class PlaidException : Exception
public PlaidException(string message, Exception innerException) : base(message, innerException) 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;
}
}
} }