using GB5Shared.ActionProcessor;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace FrameworkBLL.ActionProcessor.Handlers
{
///
/// ActionType = 2 (Webhook).
/// Sends POST request with payload to endpoint resolved from MWEBSERVICE.
/// Endpoint URL is resolved by IWebhookEndpointResolver using WebServiceId + UriParameterValue.
/// Uses IHttpClientFactory — never instantiate HttpClient directly.
///
public sealed class WebhookActionHandler : IActionHandler
{
public int ActionType => 2;
private readonly IHttpClientFactory _httpFactory;
private readonly IWebhookEndpointResolver _endpointResolver;
private readonly ILogger _logger;
public WebhookActionHandler(
IHttpClientFactory httpFactory,
IWebhookEndpointResolver endpointResolver,
ILogger logger)
{
_httpFactory = httpFactory ?? throw new ArgumentNullException(nameof(httpFactory));
_endpointResolver = endpointResolver ?? throw new ArgumentNullException(nameof(endpointResolver));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task HandleAsync(ActionEventDto dto, CancellationToken ct = default)
{
if (dto == null)
return ActionResult.Permanent("ActionEventDto cannot be null");
string? url;
try
{
url = await _endpointResolver.ResolveAsync(
dto.WebServiceId,
dto.UriParameterValue,
dto.TenantId,
dto.ConnectionName,
ct
).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Webhook endpoint resolve failed | ActionRunId={ActionRunId} WebServiceId={WebServiceId}",
dto.ActionRunId,
dto.WebServiceId);
return ActionResult.Permanent($"Endpoint resolve failed: {ex.Message}");
}
if (string.IsNullOrWhiteSpace(url))
{
_logger.LogWarning(
"Webhook endpoint missing | ActionRunId={ActionRunId} WebServiceId={WebServiceId}",
dto.ActionRunId,
dto.WebServiceId);
return ActionResult.Permanent($"No endpoint configured for WebServiceId={dto.WebServiceId}");
}
try
{
var http = _httpFactory.CreateClient("webhook");
string json;
if (dto.Payload.ValueKind == JsonValueKind.Undefined ||
dto.Payload.ValueKind == JsonValueKind.Null)
{
json = "{}";
}
else
{
json = JsonSerializer.Serialize(dto.Payload);
}
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await http
.PostAsync(url, content, ct)
.ConfigureAwait(false);
var body = await response.Content
.ReadAsStringAsync(ct)
.ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
_logger.LogInformation(
"Webhook success | ActionRunId={ActionRunId} Url={Url} Status={StatusCode}",
dto.ActionRunId,
url,
(int)response.StatusCode);
return ActionResult.Ok(body);
}
_logger.LogWarning(
"Webhook returned non-success | ActionRunId={ActionRunId} Url={Url} Status={StatusCode}",
dto.ActionRunId,
url,
(int)response.StatusCode);
// 4xx = permanent error (configuration/problem with request)
// 5xx = transient error (remote server issue)
return (int)response.StatusCode >= 500
? ActionResult.Transient($"HTTP {(int)response.StatusCode}: {body}")
: ActionResult.Permanent($"HTTP {(int)response.StatusCode}: {body}");
}
catch (HttpRequestException ex)
{
_logger.LogError(
ex,
"Webhook HTTP exception | ActionRunId={ActionRunId} Url={Url}",
dto.ActionRunId,
url);
return ActionResult.Transient(ex.Message);
}
catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)
{
_logger.LogWarning(
ex,
"Webhook timeout | ActionRunId={ActionRunId} Url={Url}",
dto.ActionRunId,
url);
return ActionResult.Transient("Webhook request timed out");
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Webhook unexpected exception | ActionRunId={ActionRunId} Url={Url}",
dto.ActionRunId,
url);
return ActionResult.Transient(ex.Message);
}
}
}
}