w4c-workflows-api/Services/Nodes/Executors/HttpRequestNodeExecutor.Body.cs
2026-09-13 19:28:47 +03:00

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