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); } } } }