bitwarden-server/src/Core/Dirt/Services/IIntegrationHandler.cs
Brant DeBow 86a68ab637
Move all event integration code to Dirt (#6757)
* Move all event integration code to Dirt

* Format to fix lint
2025-12-30 10:59:19 -05:00

115 lines
4.9 KiB
C#

using System.Globalization;
using System.Net;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
namespace Bit.Core.Dirt.Services;
public interface IIntegrationHandler
{
Task<IntegrationHandlerResult> HandleAsync(string json);
}
public interface IIntegrationHandler<T> : IIntegrationHandler
{
Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);
}
public abstract class IntegrationHandlerBase<T> : IIntegrationHandler<T>
{
public async Task<IntegrationHandlerResult> HandleAsync(string json)
{
var message = IntegrationMessage<T>.FromJson(json);
return await HandleAsync(message ?? throw new ArgumentException("IntegrationMessage was null when created from the provided JSON"));
}
public abstract Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);
protected IntegrationHandlerResult ResultFromHttpResponse(
HttpResponseMessage response,
IntegrationMessage<T> message,
TimeProvider timeProvider)
{
if (response.IsSuccessStatusCode)
{
return IntegrationHandlerResult.Succeed(message);
}
var category = ClassifyHttpStatusCode(response.StatusCode);
var failureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}";
if (category is not (IntegrationFailureCategory.RateLimited
or IntegrationFailureCategory.TransientError
or IntegrationFailureCategory.ServiceUnavailable) ||
!response.Headers.TryGetValues("Retry-After", out var values)
)
{
return IntegrationHandlerResult.Fail(message: message, category: category, failureReason: failureReason);
}
// Handle Retry-After header for rate-limited and retryable errors
DateTime? delayUntil = null;
var value = values.FirstOrDefault();
if (int.TryParse(value, out var seconds))
{
// Retry-after was specified in seconds
delayUntil = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime;
}
else if (DateTimeOffset.TryParseExact(value,
"r", // "r" is the round-trip format: RFC1123
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var retryDate))
{
// Retry-after was specified as a date
delayUntil = retryDate.UtcDateTime;
}
return IntegrationHandlerResult.Fail(
message,
category,
failureReason,
delayUntil
);
}
/// <summary>
/// Classifies an <see cref="HttpStatusCode"/> as an <see cref="IntegrationFailureCategory"/> to drive
/// retry behavior and operator-facing failure reporting.
/// </summary>
/// <param name="statusCode">The HTTP status code.</param>
/// <returns>The corresponding <see cref="IntegrationFailureCategory"/>.</returns>
protected static IntegrationFailureCategory ClassifyHttpStatusCode(HttpStatusCode statusCode)
{
var explicitCategory = statusCode switch
{
HttpStatusCode.Unauthorized => IntegrationFailureCategory.AuthenticationFailed,
HttpStatusCode.Forbidden => IntegrationFailureCategory.AuthenticationFailed,
HttpStatusCode.NotFound => IntegrationFailureCategory.ConfigurationError,
HttpStatusCode.Gone => IntegrationFailureCategory.ConfigurationError,
HttpStatusCode.MovedPermanently => IntegrationFailureCategory.ConfigurationError,
HttpStatusCode.TemporaryRedirect => IntegrationFailureCategory.ConfigurationError,
HttpStatusCode.PermanentRedirect => IntegrationFailureCategory.ConfigurationError,
HttpStatusCode.TooManyRequests => IntegrationFailureCategory.RateLimited,
HttpStatusCode.RequestTimeout => IntegrationFailureCategory.TransientError,
HttpStatusCode.InternalServerError => IntegrationFailureCategory.TransientError,
HttpStatusCode.BadGateway => IntegrationFailureCategory.TransientError,
HttpStatusCode.GatewayTimeout => IntegrationFailureCategory.TransientError,
HttpStatusCode.ServiceUnavailable => IntegrationFailureCategory.ServiceUnavailable,
HttpStatusCode.NotImplemented => IntegrationFailureCategory.PermanentFailure,
_ => (IntegrationFailureCategory?)null
};
if (explicitCategory is not null)
{
return explicitCategory.Value;
}
return (int)statusCode switch
{
>= 300 and <= 399 => IntegrationFailureCategory.ConfigurationError,
>= 400 and <= 499 => IntegrationFailureCategory.ConfigurationError,
>= 500 and <= 599 => IntegrationFailureCategory.ServiceUnavailable,
_ => IntegrationFailureCategory.ServiceUnavailable
};
}
}