Area of Concern
Server Info
- Version: CodeProject.AI 2.9.5
- Operating System & architecture: Reproduced across a Mesh pair - Windows x64 (forwarding server) and Debian/Raspberry Pi Linux-Arm64 (receiving server)
- System RAM: 32Gb (Windows forwarding server)
- Video card: N/A - not relevant to this bug
- Using Docker? No (native installs on both servers)
Describe the bug
When a mesh request is received img is None - no image is set over the mesh request.
Detail
When Mesh request forwarding is enabled (AllowRequestForwarding: true) and a multipart/form-data request (e.g. a POST /v1/vision/detection image upload) is forwarded from one server to another via ProxyController.ForwardAsync(), the image/file data does not survive the forward. The receiving server gets a syntactically valid detect command but with no files attached, so any module that requires an image (e.g. ObjectDetectionCoral) receives img = None and fails or returns "No image provided".
Cause
I believe the cause is (src/server/Controllers/ProxyController.cs; ForwardAsync()):
HttpRequestMessage newRequest = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StreamContent(originalRequest.Body)
};
foreach (KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues> header in originalRequest.Headers)
newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
This loop copies every header from the original incoming request - including Content-Type, which for a multipart upload carries the required boundary=... parameter - onto newRequest.Headers (the HttpRequestMessage header collection).
But content-specific headers (Content-Type, Content-Length, etc.) are only honoured by HttpClient when set on newRequest.Content.Headers (HttpContentHeaders), not on HttpRequestMessage.Headers.
Since StreamContent is constructed here with no explicit Content.Headers.ContentType assignment, the outgoing forwarded request never gets a usable Content-Type: multipart/form-data; boundary=... header on the wire. The receiving server's Kestrel pipeline then cannot recognise/parse the body as form data, Request.Form.Files comes back empty, and any file/image field is lost.
Note, there is a secondary effect that makes this harder to diagnose! lol. In DispatchRemoteRequest(), any forwarded response with success: false has it's recorded elapsed time being overridden to a flat 30 seconds before being fed into _meshManager.AddResponseTime(...):
if (!responseObject!.ContainsKey("success") || !(bool)responseObject["success"]!)
{
elapsedMs = 30_000;
}
Im guessing this is to push dodgy remote servers out of routing contention? However, because the above bug makes every forwarded image request fail, every one gets this fake 30s timing recorded!! That so threw me for a while because the Mesh dashboard's tracked effectiveResponseTime for the route climbs toward 30 seconds and looks exactly like a network timeout/slow-server problem, when the real round trip is actually fast (a few hundred ms) and the actual issue is the lost multipart boundary.
Expected behavior
A forwarded multipart/form-data request (including any attached image/file) should arrive at the target Mesh server intact, with the same Content-Type/boundary as the original request, so the receiving server's module can process the image normally.
Screenshots
N/A
Additional context
My suggested fix is (Been out of cs for a few years, so no ide setup to try/debug sorry :-( ), in ForwardAsync(): fall back to newRequest.Content.Headers for any header that newRequest.Headers.TryAddWithoutValidation rejects (which correctly happens for known content-only headers like Content-Type/Content-Length). IE:
foreach (KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues> header in originalRequest.Headers)
{
if (!newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()))
newRequest.Content!.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
}
This ensures Content-Type (with its multipart boundary) and Content-Length land on the HttpContent object where HttpClient actually reads them from when serialising the outgoing request. Once fixed, the secondary 30s-penalty behaviour should also stop firing for image-forwarding requests, since they'll start succeeding.
Reproduction steps:
- Set up two CodeProject.AI Server instances with Mesh enabled, one forwarding-allowed (
AllowRequestForwarding: true), one accepting (AcceptForwardedRequests: true), each aware of the other via KnownMeshHostnames.
- Ensure the forwarding server's mesh routing selects the remote server for
vision/detection (e.g. by having no local module capable of handling that route, or by response-time metrics favouring the remote server).
- POST a
multipart/form-data image detection request to the forwarding server's /v1/vision/detection endpoint.
- On the receiving server, observe the module log: the
detect command arrives with no files/empty image, even though a valid image was attached to the original request.
Area of Concern
Server Info
Describe the bug
When a mesh request is received img is None - no image is set over the mesh request.
Detail
When Mesh request forwarding is enabled (
AllowRequestForwarding: true) and amultipart/form-datarequest (e.g. aPOST /v1/vision/detectionimage upload) is forwarded from one server to another viaProxyController.ForwardAsync(), the image/file data does not survive the forward. The receiving server gets a syntactically validdetectcommand but with no files attached, so any module that requires an image (e.g. ObjectDetectionCoral) receivesimg = Noneand fails or returns "No image provided".Cause
I believe the cause is (src/server/Controllers/ProxyController.cs;
ForwardAsync()):This loop copies every header from the original incoming request - including
Content-Type, which for a multipart upload carries the requiredboundary=...parameter - ontonewRequest.Headers(theHttpRequestMessageheader collection).But content-specific headers (
Content-Type,Content-Length, etc.) are only honoured byHttpClientwhen set onnewRequest.Content.Headers(HttpContentHeaders), not onHttpRequestMessage.Headers.Since
StreamContentis constructed here with no explicitContent.Headers.ContentTypeassignment, the outgoing forwarded request never gets a usableContent-Type: multipart/form-data; boundary=...header on the wire. The receiving server's Kestrel pipeline then cannot recognise/parse the body as form data,Request.Form.Filescomes back empty, and any file/image field is lost.Note, there is a secondary effect that makes this harder to diagnose! lol. In
DispatchRemoteRequest(), any forwarded response withsuccess: falsehas it's recorded elapsed time being overridden to a flat 30 seconds before being fed into_meshManager.AddResponseTime(...):Im guessing this is to push dodgy remote servers out of routing contention? However, because the above bug makes every forwarded image request fail, every one gets this fake 30s timing recorded!! That so threw me for a while because the Mesh dashboard's tracked
effectiveResponseTimefor the route climbs toward 30 seconds and looks exactly like a network timeout/slow-server problem, when the real round trip is actually fast (a few hundred ms) and the actual issue is the lost multipart boundary.Expected behavior
A forwarded
multipart/form-datarequest (including any attached image/file) should arrive at the target Mesh server intact, with the sameContent-Type/boundary as the original request, so the receiving server's module can process the image normally.Screenshots
N/A
Additional context
My suggested fix is (Been out of cs for a few years, so no ide setup to try/debug sorry :-( ), in
ForwardAsync(): fall back tonewRequest.Content.Headersfor any header thatnewRequest.Headers.TryAddWithoutValidationrejects (which correctly happens for known content-only headers likeContent-Type/Content-Length). IE:This ensures
Content-Type(with its multipart boundary) andContent-Lengthland on theHttpContentobject whereHttpClientactually reads them from when serialising the outgoing request. Once fixed, the secondary 30s-penalty behaviour should also stop firing for image-forwarding requests, since they'll start succeeding.Reproduction steps:
AllowRequestForwarding: true), one accepting (AcceptForwardedRequests: true), each aware of the other viaKnownMeshHostnames.vision/detection(e.g. by having no local module capable of handling that route, or by response-time metrics favouring the remote server).multipart/form-dataimage detection request to the forwarding server's/v1/vision/detectionendpoint.detectcommand arrives with no files/empty image, even though a valid image was attached to the original request.