using System.Net.Http.Headers; using Elsa.Extensions; using Elsa.Http.ContentWriters; using Elsa.Http.UIHints; using Elsa.Workflows; using Elsa.Workflows.Attributes; using Elsa.Workflows.UIHints; using Elsa.Workflows.Models; using HttpHeaders = Elsa.Http.Models.HttpHeaders; namespace Elsa.Http; /// /// Base class for activities that send HTTP requests. /// [Output(IsSerializable = false)] 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.", UIHandler = typeof(HttpContentTypeOptionsProvider), 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!; /// /// A value that allows to add the Authorization header without validation. /// [Input(Description = "A value that allows to add the Authorization header without validation.", Category = "Security")] public Input DisableAuthorizationHeaderValidation { 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 HttpHeaders()); /// /// 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); var addAuthorizationWithoutValidation = DisableAuthorizationHeaderValidation.GetOrDefault(context); if (!string.IsNullOrWhiteSpace(authorization)) if(addAuthorizationWithoutValidation) request.Headers.TryAddWithoutValidation("Authorization", authorization); else 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 factories = context.GetServices(); var factory = SelectContentWriter(contentType, factories); request.Content = factory.CreateHttpContent(content, contentType); } return request; } private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable factories) { if (string.IsNullOrWhiteSpace(contentType)) return new JsonContentFactory(); var parsedContentType = new System.Net.Mime.ContentType(contentType); return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory(); } }