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 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 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(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}'"; } } /// /// 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. /// private async Task 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; } /// Sends an input binary attachment as the raw request body. private async Task 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; } /// /// 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. /// 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; } }