< Summary

Information
Class: Elsa.Http.DownloadHttpFile
Assembly: Elsa.Http
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Http/Activities/DownloadHttpFile.cs
Line coverage
86%
Covered lines: 96
Uncovered lines: 15
Coverable lines: 111
Total lines: 290
Line coverage: 86.4%
Branch coverage
54%
Covered branches: 25
Total branches: 46
Branch coverage: 54.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Http/Activities/DownloadHttpFile.cs

#LineLine coverage
 1using System.Net.Http.Headers;
 2using System.Reflection;
 3using System.Runtime.CompilerServices;
 4using Elsa.Extensions;
 5using Elsa.Http.ContentWriters;
 6using Elsa.Http.UIHints;
 7using Elsa.Workflows;
 8using Elsa.Workflows.Attributes;
 9using Elsa.Workflows.UIHints;
 10using Elsa.Workflows.Models;
 11using Microsoft.Extensions.Logging;
 12
 13namespace Elsa.Http;
 14
 15/// <summary>
 16/// An activity that downloads a file from a given URL.
 17/// </summary>
 18[Activity("Elsa", "HTTP", "Downloads a file from a given URL.", DisplayName = "Download File", Kind = ActivityKind.Task)
 19[Output(IsSerializable = false)]
 20public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValueProvider
 21{
 22    /// <inheritdoc />
 2123    public DownloadHttpFile([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, 
 24    {
 2125    }
 26
 27    /// <summary>
 28    /// The URL to download the file from.
 29    /// </summary>
 30    [Input(DisplayName = "URL", Description = "The URL to download the file from.")]
 8431    public Input<Uri?> Url { get; set; } = null!;
 32
 33    /// <summary>
 34    /// The HTTP method to use when sending the request.
 35    /// </summary>
 36    [Input(
 37        Description = "The HTTP method to use when sending the request.",
 38        Options = new[]
 39        {
 40            "GET", "POST", "PUT"
 41        },
 42        DefaultValue = "GET",
 43        UIHint = InputUIHints.DropDown
 44    )]
 10545    public Input<string> Method { get; set; } = new("GET");
 46
 47    /// <summary>
 48    /// A list of expected status codes to handle.
 49    /// </summary>
 50    [Input(
 51        Description = "A list of expected status codes to handle.",
 52        UIHint = InputUIHints.MultiText,
 53        DefaultValueProvider = typeof(FlowSendHttpRequest)
 54    )]
 8255    public Input<ICollection<int>> ExpectedStatusCodes { get; set; } = null!;
 56
 57    /// <summary>
 58    /// The content to send with the request. Can be a string, an object, a byte array or a stream.
 59    /// </summary>
 60    [Input(Name = "Content", Description = "The content to send with the request. Can be a string, an object, a byte arr
 6361    public Input<object?> RequestContent { get; set; } = null!;
 62
 63    /// <summary>
 64    /// The content type to use when sending the request.
 65    /// </summary>
 66    [Input(
 67        DisplayName = "Content Type",
 68        Description = "The content type to use when sending the request.",
 69        UIHandler = typeof(HttpContentTypeOptionsProvider),
 70        UIHint = InputUIHints.DropDown
 71    )]
 6372    public Input<string?> RequestContentType { get; set; } = null!;
 73
 74    /// <summary>
 75    /// The Authorization header value to send with the request.
 76    /// </summary>
 77    /// <example>Bearer {some-access-token}</example>
 78    [Input(Description = "The Authorization header value to send with the request. For example: Bearer {some-access-toke
 8479    public Input<string?> Authorization { get; set; } = null!;
 80
 81    /// <summary>
 82    /// A value that allows to add the Authorization header without validation.
 83    /// </summary>
 84    [Input(Description = "A value that allows to add the Authorization header without validation.", Category = "Security
 6385    public Input<bool> DisableAuthorizationHeaderValidation { get; set; } = null!;
 86
 87    /// <summary>
 88    /// The headers to send along with the request.
 89    /// </summary>
 90    [Input(
 91        Description = "The headers to send along with the request.",
 92        UIHint = InputUIHints.JsonEditor,
 93        Category = "Advanced"
 94    )]
 8495    public Input<HttpHeaders?> RequestHeaders { get; set; } = new(new HttpHeaders());
 96
 97    /// <summary>
 98    /// The HTTP response.
 99    /// </summary>
 100    [Output(IsSerializable = false)]
 40101    public Output<HttpResponseMessage> Response { get; set; } = null!;
 102
 103    /// <summary>
 104    /// The HTTP response status code
 105    /// </summary>
 106    [Output(Description = "The HTTP response status code")]
 40107    public Output<int> StatusCode { get; set; } = null!;
 108
 109    /// <summary>
 110    /// The downloaded content stream, if any.
 111    /// </summary>
 112    [Output(Description = "The downloaded content stream, if any.", IsSerializable = false)]
 40113    public Output<Stream?> ResponseContentStream { get; set; } = null!;
 114
 115    /// <summary>
 116    /// The downloaded content bytes, if any.
 117    /// </summary>
 118    [Output(Description = "The downloaded content bytes, if any.", IsSerializable = false)]
 40119    public Output<byte[]?> ResponseContentBytes { get; set; } = null!;
 120
 121    /// <summary>
 122    /// The response headers that were received.
 123    /// </summary>
 124    [Output(Description = "The response headers that were received.")]
 40125    public Output<HttpHeaders?> ResponseHeaders { get; set; } = null!;
 126
 127    /// <summary>
 128    /// The response content headers that were received.
 129    /// </summary>
 130    [Output(DisplayName = "Content Headers", Description = "The response content headers that were received.")]
 40131    public Output<HttpHeaders?> ResponseContentHeaders { get; set; } = null!;
 132
 133    /// <inheritdoc />
 134    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
 135    {
 21136        await TrySendAsync(context);
 21137    }
 138
 139    private async Task TrySendAsync(ActivityExecutionContext context)
 140    {
 21141        var request = PrepareRequest(context);
 21142        var logger = (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType()));
 21143        var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
 21144        var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase));
 21145        var cancellationToken = context.CancellationToken;
 146
 147        try
 148        {
 21149            var response = await httpClient.SendAsync(request, cancellationToken);
 19150            var file = await GetFileFromResponse(context, response, request);
 19151            var statusCode = (int)response.StatusCode;
 19152            var responseHeaders = new HttpHeaders(response.Headers);
 19153            var responseContentHeaders = new HttpHeaders(response.Content.Headers);
 154
 19155            context.Set(Response, response);
 19156            context.Set(ResponseContentStream, file?.Stream);
 19157            context.Set(Result, file);
 19158            context.Set(StatusCode, statusCode);
 19159            context.Set(ResponseHeaders, responseHeaders);
 19160            context.Set(ResponseContentHeaders, responseContentHeaders);
 19161            if (ResponseContentBytes.HasTarget(context)) context.Set(ResponseContentBytes, file?.GetBytes());
 162
 19163            await HandleResponseAsync(context, response);
 19164        }
 1165        catch (HttpRequestException e)
 166        {
 1167            logger.LogWarning(e, "An error occurred while sending an HTTP request");
 1168            context.AddExecutionLogEntry("Error", e.Message, payload: new
 1169            {
 1170                StackTrace = e.StackTrace
 1171            });
 1172            context.JournalData.Add("Error", e.Message);
 1173            await HandleRequestExceptionAsync(context, e);
 1174        }
 1175        catch (TaskCanceledException e)
 176        {
 1177            logger.LogWarning(e, "An error occurred while sending an HTTP request");
 1178            context.AddExecutionLogEntry("Error", e.Message, payload: new
 1179            {
 1180                StackTrace = e.StackTrace
 1181            });
 1182            context.JournalData.Add("Cancelled", true);
 1183            await HandleTaskCanceledExceptionAsync(context, e);
 184        }
 21185    }
 186
 187    /// <summary>
 188    /// Handles the response.
 189    /// </summary>
 190    private async Task HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
 191    {
 19192        var expectedStatusCodes = ExpectedStatusCodes.GetOrDefault(context) ?? new List<int>(0);
 19193        var statusCode = (int)response.StatusCode;
 19194        var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode);
 19195        var outcome = expectedStatusCodes.Any() ? hasMatchingStatusCode ? statusCode.ToString() : "Unmatched status code
 19196        var outcomes = new List<string>();
 197
 19198        if (outcome != null)
 19199            outcomes.Add(outcome);
 200
 19201        outcomes.Add("Done");
 19202        await context.CompleteActivityWithOutcomesAsync(outcomes.ToArray());
 19203    }
 204
 205    /// <summary>
 206    /// Handles an exception that occurred while sending the request.
 207    /// </summary>
 208    private async Task HandleRequestExceptionAsync(ActivityExecutionContext context, HttpRequestException exception)
 209    {
 1210        await context.CompleteActivityWithOutcomesAsync("Failed to connect");
 1211    }
 212
 213    /// <summary>
 214    /// Handles <see cref="TaskCanceledException"/> that occurred while sending the request.
 215    /// </summary>
 216    private async Task HandleTaskCanceledExceptionAsync(ActivityExecutionContext context, TaskCanceledException exceptio
 217    {
 1218        await context.CompleteActivityWithOutcomesAsync("Timeout");
 1219    }
 220
 221    private async Task<HttpFile?> GetFileFromResponse(ActivityExecutionContext context, HttpResponseMessage httpResponse
 222    {
 19223        var httpContent = httpResponse.Content;
 19224        if (!HasContent(httpContent))
 3225            return null;
 226
 16227        var cancellationToken = context.CancellationToken;
 16228        var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken);
 16229        var responseHeaders = httpResponse.Headers;
 16230        var contentHeaders = httpContent.Headers;
 16231        var contentType = contentHeaders.ContentType?.MediaType!;
 16232        var filename = contentHeaders.ContentDisposition?.FileName ?? httpRequestMessage.RequestUri!.Segments.LastOrDefa
 16233        var eTag = responseHeaders.ETag?.Tag;
 234
 16235        return new HttpFile(contentStream, filename, contentType, eTag);
 19236    }
 237
 19238    private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0;
 239
 240    private HttpRequestMessage PrepareRequest(ActivityExecutionContext context)
 241    {
 21242        var method = Method.GetOrDefault(context) ?? "GET";
 21243        var url = Url.Get(context);
 21244        var request = new HttpRequestMessage(new HttpMethod(method), url);
 21245        var headers = context.GetHeaders(RequestHeaders);
 21246        var authorization = Authorization.GetOrDefault(context);
 21247        var addAuthorizationWithoutValidation = DisableAuthorizationHeaderValidation.GetOrDefault(context);
 248
 21249        if (!string.IsNullOrWhiteSpace(authorization))
 2250            if (addAuthorizationWithoutValidation)
 0251                request.Headers.TryAddWithoutValidation("Authorization", authorization);
 252            else
 2253                request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
 254
 42255        foreach (var header in headers)
 0256            request.Headers.Add(header.Key, header.Value.AsEnumerable());
 257
 21258        var contentType = RequestContentType.GetOrDefault(context);
 21259        var content = RequestContent.GetOrDefault(context);
 260
 21261        if (contentType != null && content != null)
 262        {
 0263            var factories = context.GetServices<IHttpContentFactory>();
 0264            var factory = SelectContentWriter(contentType, factories);
 0265            request.Content = factory.CreateHttpContent(content, contentType);
 266        }
 267
 21268        return request;
 269    }
 270
 271    private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable<IHttpContentFactory> factories)
 272    {
 0273        if (string.IsNullOrWhiteSpace(contentType))
 0274            return new JsonContentFactory();
 275
 0276        var parsedContentType = new System.Net.Mime.ContentType(contentType);
 0277        return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == par
 278    }
 279
 280    object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property)
 281    {
 0282        if (property.Name == nameof(ExpectedStatusCodes))
 0283            return new List<int>
 0284            {
 0285                200
 0286            };
 287
 0288        return null!;
 289    }
 290}