feat(api,infra): implement GlobalExceptionHandler and enhance PlaidException for detailed error handling
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user