From 4fe6479f98e0ce14f4ea8db7bcaa9ca5eff4e72f Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Sat, 11 Jul 2026 11:49:03 +1000 Subject: [PATCH] Fix mesh-forwarded image requests losing body data Three stacked bugs caused every image sent to a mesh-forwarded route (e.g. POST /v1/vision/detection) to arrive at the receiving server with no image data, whether from a real client or the mesh's own periodic route-probing: ForwardAsync() copied Content-Type/Content-Length onto HttpRequestMessage.Headers instead of Content.Headers, so HttpClient never sent a usable Content-Type (with multipart boundary) on the wire. The receiving Kestrel pipeline couldn't parse the body as multipart form data and Request.Form.Files came back empty. Forwarding exceptions were logged via Debug.WriteLine, a no-op in Release builds and never routed through CodeProject.AI's own log system, so failures were invisible. Wired DispatchRemoteRequest's catch block up to ILogger instead, and surfaced ex.Message directly in the error response. With both above fixed, forwarding started throwing "Sent 0 request content bytes, but Content-Length promised NNNNN" - the request body stream was already consumed upstream by the time ForwardAsync() read it. Fixed by enabling request buffering as the first middleware in Startup.Configure() and rewinding the body position immediately before building the forwarded request. Also fixes dashboard's "Accepting Requests" line (both server-side in MeshSummary.cs and client-side in dashboard.js) read AllowRequestForwarding instead of AcceptForwardedRequests, so a receive-only mesh server showed as not accepting forwarded requests even while correctly processing them. --- CodeProject.AI.sln | 3 +-- src/server/Controllers/ProxyController.cs | 26 +++++++++++++++++------ src/server/Mesh/MeshSummary.cs | 2 +- src/server/Startup.cs | 11 ++++++++++ src/server/wwwroot/assets/dashboard.js | 2 +- 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/CodeProject.AI.sln b/CodeProject.AI.sln index 35ac3231..55a419a6 100644 --- a/CodeProject.AI.sln +++ b/CodeProject.AI.sln @@ -1,5 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 diff --git a/src/server/Controllers/ProxyController.cs b/src/server/Controllers/ProxyController.cs index 06a50715..e42f16c6 100644 --- a/src/server/Controllers/ProxyController.cs +++ b/src/server/Controllers/ProxyController.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using CodeProject.AI.SDK.API; @@ -65,6 +66,7 @@ public class ProxyController : ControllerBase private readonly TriggerTaskRunner _commandRunner; private readonly MeshManager _meshManager; private readonly ModuleProcessServices _moduleProcessService; + private readonly ILogger _logger; private bool _verbose = false; @@ -79,13 +81,15 @@ public class ProxyController : ControllerBase /// The command runner /// The mesh manager /// The module process service + /// The logger public ProxyController(CommandDispatcher dispatcher, BackendRouteMap routeMap, IOptions ModuleCollectionOptions, IOptions triggersConfig, TriggerTaskRunner commandRunner, MeshManager meshManager, - ModuleProcessServices moduleProcessService) + ModuleProcessServices moduleProcessService, + ILogger logger) { _dispatcher = dispatcher; _routeMap = routeMap; @@ -94,6 +98,7 @@ public ProxyController(CommandDispatcher dispatcher, _commandRunner = commandRunner; _meshManager = meshManager; _moduleProcessService = moduleProcessService; + _logger = logger; } /// @@ -386,11 +391,10 @@ public IActionResult ApiSummary() } catch (Exception ex) { - Debug.WriteLine($"Error in DispatchRemoteRequest ({server.Status.Hostname}): {ex}"); + _logger.LogError(ex, "Error in DispatchRemoteRequest ({Hostname})", server.Status.Hostname); - // Bump the response to 30s to push this server out of contention. Maybe also add - // exception info to the message - error = $"Exception when forwarding request to {server.Status.Hostname}"; + // Bump the response to 30s to push this server out of contention. + error = $"Exception when forwarding request to {server.Status.Hostname}: {ex.Message}"; elapsedMs = 30_000; } finally @@ -521,6 +525,13 @@ public IActionResult ApiSummary() private async Task ForwardAsync(MeshServerRoutingEntry server) { HttpRequest originalRequest = Request; + + // The body may already have been read once by ASP.NET Core's own model-binding + // pipeline before this point. EnableBuffering() (see Startup.cs) makes the body + // seekable so we can safely rewind it here and forward the full original content. + if (originalRequest.Body.CanSeek) + originalRequest.Body.Position = 0; + int? port = originalRequest.Host.Port; string portString = port.HasValue ? $":{port}" : string.Empty; var queryString = originalRequest.QueryString; @@ -538,7 +549,10 @@ private async Task ForwardAsync(MeshServerRoutingEntry serv }; foreach (KeyValuePair header in originalRequest.Headers) - newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); + { + if (!newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable())) + newRequest.Content!.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); + } newRequest.Headers.Add(CPAI_Forwarded_Header, "true"); diff --git a/src/server/Mesh/MeshSummary.cs b/src/server/Mesh/MeshSummary.cs index 30a67019..df9b2ccb 100644 --- a/src/server/Mesh/MeshSummary.cs +++ b/src/server/Mesh/MeshSummary.cs @@ -36,7 +36,7 @@ public string Summary } summary.AppendLine($"{indent}Active: {IsActive}"); summary.AppendLine($"{indent}Forwarding Requests: {Status.AllowRequestForwarding}"); - summary.AppendLine($"{indent}Accepting Requests: {Status.AllowRequestForwarding}"); + summary.AppendLine($"{indent}Accepting Requests: {Status.AcceptForwardedRequests}"); if (Status.KnownHostnames is not null) { diff --git a/src/server/Startup.cs b/src/server/Startup.cs index d2864c10..3db3bc5b 100644 --- a/src/server/Startup.cs +++ b/src/server/Startup.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -137,6 +138,16 @@ public void Configure(IApplicationBuilder app, _versionConfig = versionConfig.Value; _logger = logger; + // Enable request body buffering globally, as early as possible, so any code + // downstream (in particular ProxyController's mesh-forwarding logic) can safely + // re-read/replay the request body even after ASP.NET Core's own model-binding + // infrastructure has already read from it once. + app.Use(async (context, next) => + { + context.Request.EnableBuffering(); + await next(); + }); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/src/server/wwwroot/assets/dashboard.js b/src/server/wwwroot/assets/dashboard.js index a8902dbd..84b96057 100755 --- a/src/server/wwwroot/assets/dashboard.js +++ b/src/server/wwwroot/assets/dashboard.js @@ -546,7 +546,7 @@ function meshServerSummary(server, isLocal) { summary += `${indent}All Addresses: ${server.allIPAddresses.join(", ")}\n`; summary += `${indent}Active: ${activeStr(server.isActive)}\n`; summary += `${indent}Forwarding Requests: ${activeStr(server.status.allowRequestForwarding)}\n`; - summary += `${indent}Accepting Requests: ${activeStr(server.status.allowRequestForwarding)}\n`; + summary += `${indent}Accepting Requests: ${activeStr(server.status.acceptForwardedRequests)}\n`; // Routes (may be an empty list) let hideRoutes = document.getElementById("showMeshRoutes") &&