using System.Net.Http.Headers; using Elsa.Extensions; using Elsa.Http.ContentWriters; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders; namespace Elsa.Http; /// /// Base class for activities that send HTTP requests. /// public abstract class SendHttpRequestBase : Activity { /// protected SendHttpRequestBase(string? source = default, int? line = default) : base(source, line) { } /// /// The URL to send the request to. /// [Input] public Input Url { get; set; } = default!; /// /// The HTTP method to use when sending the request. /// [Input( Description = "The HTTP method to use when sending the request.", Options = new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" }, DefaultValue = "GET", UIHint = InputUIHints.Dropdown )] public Input Method { get; set; } = new("GET"); /// /// The content to send with the request. Can be a string, an object, a byte array or a stream. /// [Input(Description = "The content to send with the request. Can be a string, an object, a byte array or a stream.")] public Input Content { get; set; } = default!; /// /// The content type to use when sending the request. /// [Input( Description = "The content type to use when sending the request.", Options = new[] { "", "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" }, UIHint = InputUIHints.Dropdown )] public Input ContentType { get; set; } = default!; /// /// The Authorization header value to send with the request. /// /// Bearer {some-access-token} [Input( Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}", Category = "Security" )] public Input Authorization { get; set; } = default!; /// /// The headers to send along with the request. /// [Input(Description = "The headers to send along with the request.", Category = "Advanced")] public Input RequestHeaders { get; set; } = new(new HttpRequestHeaders()); /// /// The parsed content, if any. /// [Output(Description = "The parsed content, if any.")] public Output ParsedContent { get; set; } = default!; /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { await TrySendAsync(context); } /// /// Handles the response. /// protected abstract ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response); /// /// Handles an exception that occurred while sending the request. /// protected abstract ValueTask HandleRequestExceptionAsync(ActivityExecutionContext context, HttpRequestException exception); /// /// Handles that occurred while sending the request. /// protected abstract ValueTask HandleTaskCanceledExceptionAsync(ActivityExecutionContext context, TaskCanceledException exception); private async Task TrySendAsync(ActivityExecutionContext context) { var request = PrepareRequest(context); var httpClientFactory = context.GetRequiredService(); var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase)); var cancellationToken = context.CancellationToken; try { var response = await httpClient.SendAsync(request, cancellationToken); var parsedContent = await ParseContentAsync(context, response.Content); context.Set(Result, response); context.Set(ParsedContent, parsedContent); await HandleResponseAsync(context, response); } catch(HttpRequestException e) { context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace }); context.JournalData.Add("Error", e.Message); await HandleRequestExceptionAsync(context, e); } catch (TaskCanceledException e) { context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace }); context.JournalData.Add("Cancelled", true); await HandleTaskCanceledExceptionAsync(context, e); } } private async Task ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent) { if (!HasContent(httpContent)) return null; var cancellationToken = context.CancellationToken; var targetType = ParsedContent.GetTargetType(context); var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken); var contentType = httpContent.Headers.ContentType?.MediaType!; targetType ??= contentType switch { "application/json" => typeof(object), _ => typeof(string) }; return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken); } private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0; private HttpRequestMessage PrepareRequest(ActivityExecutionContext context) { var method = Method.GetOrDefault(context) ?? "GET"; var url = Url.Get(context); var request = new HttpRequestMessage(new HttpMethod(method), url); var headers = context.GetHeaders(RequestHeaders); var authorization = Authorization.GetOrDefault(context); if (!string.IsNullOrWhiteSpace(authorization)) request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization); foreach (var header in headers) request.Headers.Add(header.Key, header.Value.AsEnumerable()); var contentType = ContentType.GetOrDefault(context); var content = Content.GetOrDefault(context); if (contentType != null && content != null) { var contentWriters = context.GetServices(); var contentWriter = SelectContentWriter(contentType, contentWriters); request.Content = contentWriter.CreateHttpContent(content, contentType); } return request; } private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable requestContentWriters) => string.IsNullOrWhiteSpace(contentType) ? new JsonContentFactory() : requestContentWriters.First(w => w.SupportsContentType(contentType)); }