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