163 lines
6 KiB
C#
163 lines
6 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
using w4c_workflows.Models.Credentials;
|
|
using w4c_workflows.Models.Nodes;
|
|
using w4c_workflows.Services.Credentials;
|
|
using w4c_workflows.Services.Nodes.Binary;
|
|
using w4c_workflows.Services.Security;
|
|
|
|
namespace w4c_workflows.Services.Nodes.Executors;
|
|
|
|
// Request-shaping half of the HTTP Request node: static/pagination headers and
|
|
// the JSON/form/raw/multipart/binary body builders.
|
|
|
|
public sealed partial class HttpRequestNodeExecutor
|
|
{
|
|
// ------------------------------------------------------------------ request
|
|
|
|
private static void ApplyHeaders(
|
|
NodeExecutionContext context,
|
|
HttpRequestMessage request,
|
|
IReadOnlyDictionary<string, string> overrides)
|
|
{
|
|
if (NodeValueAccess.ReadBool(context.Parameters, "sendHeaders") && context.Parameters["headers"] is JsonArray headers)
|
|
{
|
|
foreach (var entry in headers)
|
|
{
|
|
if (entry is not JsonObject header)
|
|
continue;
|
|
|
|
var name = NodeValueAccess.ReadString(header, "name");
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
continue;
|
|
|
|
request.Headers.TryAddWithoutValidation(name, NodeValueAccess.ReadString(header, "value") ?? string.Empty);
|
|
}
|
|
}
|
|
|
|
// Pagination header overrides win over the static header list.
|
|
foreach (var (name, value) in overrides)
|
|
{
|
|
request.Headers.Remove(name);
|
|
request.Headers.TryAddWithoutValidation(name, value);
|
|
}
|
|
}
|
|
|
|
private async Task<string?> ApplyBodyAsync(
|
|
NodeExecutionContext context, JsonNode? body, HttpRequestMessage request, CancellationToken ct)
|
|
{
|
|
var contentType = NodeValueAccess.ReadString(context.Parameters, "bodyContentType") ?? "json";
|
|
|
|
switch (contentType)
|
|
{
|
|
case "json":
|
|
request.Content = new StringContent(body?.ToJsonString() ?? "null", Encoding.UTF8, "application/json");
|
|
return null;
|
|
|
|
case "form":
|
|
if (body is not JsonObject form)
|
|
return "a form body must be a JSON object";
|
|
request.Content = new FormUrlEncodedContent(
|
|
form.Select(pair => new KeyValuePair<string, string>(pair.Key, NodeValueAccess.ScalarText(pair.Value))));
|
|
return null;
|
|
|
|
case "raw":
|
|
request.Content = new StringContent(NodeValueAccess.ScalarText(body), Encoding.UTF8, "text/plain");
|
|
return null;
|
|
|
|
case "multipart":
|
|
return await ApplyMultipartAsync(context, body, request, ct);
|
|
|
|
case "binary":
|
|
return await ApplyBinaryAsync(context, request, ct);
|
|
|
|
default:
|
|
return $"unknown bodyContentType '{contentType}'";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a multipart body from the declared fields plus one input binary
|
|
/// attachment. The attachment is streamed from the binary store, so the file
|
|
/// never has to be loaded into item JSON first.
|
|
/// </summary>
|
|
private async Task<string?> ApplyMultipartAsync(
|
|
NodeExecutionContext context, JsonNode? body, HttpRequestMessage request, CancellationToken ct)
|
|
{
|
|
var form = new MultipartFormDataContent();
|
|
if (body is JsonObject fields)
|
|
{
|
|
foreach (var (name, value) in fields)
|
|
form.Add(new StringContent(NodeValueAccess.ScalarText(value), Encoding.UTF8), name);
|
|
}
|
|
|
|
var property = NodeValueAccess.ReadString(context.Parameters, "binaryField");
|
|
if (string.IsNullOrWhiteSpace(property))
|
|
{
|
|
request.Content = form;
|
|
return null;
|
|
}
|
|
|
|
var attachment = ResolveInputBinary(context, property, out var error);
|
|
if (attachment == null)
|
|
{
|
|
form.Dispose();
|
|
return error;
|
|
}
|
|
|
|
var stream = await _binary.OpenAsync(attachment.AssetId, ct);
|
|
if (stream == null)
|
|
{
|
|
form.Dispose();
|
|
return $"binary asset '{attachment.AssetId}' was not found in the binary store";
|
|
}
|
|
|
|
var file = new StreamContent(stream);
|
|
if (!string.IsNullOrWhiteSpace(attachment.MimeType))
|
|
file.Headers.ContentType = new MediaTypeHeaderValue(attachment.MimeType);
|
|
form.Add(file, property, attachment.FileName ?? property);
|
|
|
|
request.Content = form;
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Sends an input binary attachment as the raw request body.</summary>
|
|
private async Task<string?> ApplyBinaryAsync(
|
|
NodeExecutionContext context, HttpRequestMessage request, CancellationToken ct)
|
|
{
|
|
var property = NodeValueAccess.ReadString(context.Parameters, "binaryField") ?? DefaultBinaryProperty;
|
|
var attachment = ResolveInputBinary(context, property, out var error);
|
|
if (attachment == null)
|
|
return error;
|
|
|
|
var stream = await _binary.OpenAsync(attachment.AssetId, ct);
|
|
if (stream == null)
|
|
return $"binary asset '{attachment.AssetId}' was not found in the binary store";
|
|
|
|
var content = new StreamContent(stream);
|
|
content.Headers.ContentType = new MediaTypeHeaderValue(
|
|
string.IsNullOrWhiteSpace(attachment.MimeType) ? "application/octet-stream" : attachment.MimeType);
|
|
request.Content = content;
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up a named binary attachment on the first input item. A missing item
|
|
/// or property is a body-configuration error, not an empty body.
|
|
/// </summary>
|
|
private static BinaryAttachment? ResolveInputBinary(
|
|
NodeExecutionContext context, string property, out string? error)
|
|
{
|
|
error = null;
|
|
var item = context.Input(0).FirstOrDefault();
|
|
if (item?.Binary == null || !item.Binary.TryGetValue(property, out var attachment))
|
|
{
|
|
error = $"the input item has no binary field '{property}'";
|
|
return null;
|
|
}
|
|
|
|
return attachment;
|
|
}
|
|
}
|