From 82dfaee56f295875752da4de792a1bc6f962497b Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:07:30 +0200 Subject: [PATCH] fix(delivery): stop the 422 retry loop on failed delivery items A FAILED delivery item never sets fileCount/totalBytes, and the worker's Dictionary-based JSON payload serializes those as explicit nulls (the WhenWritingNull option only suppresses nulls on POCO properties, not dictionary values). The server's schema allowed the fields to be omitted but not null, so the report was permanently rejected with 422 - and the worker only ever treated 409/404 as terminal, so it retried the same malformed payload forever across restarts. --- RenderWorker/VFXReviewWorker/ApiClient.cs | 8 ++++++-- lib/render-pipeline/deliveries.ts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/RenderWorker/VFXReviewWorker/ApiClient.cs b/RenderWorker/VFXReviewWorker/ApiClient.cs index 76051ba..022ffe0 100644 --- a/RenderWorker/VFXReviewWorker/ApiClient.cs +++ b/RenderWorker/VFXReviewWorker/ApiClient.cs @@ -128,11 +128,15 @@ public sealed class ApiClient }; var res = await _http.PostAsJsonAsync(path, payload, Json, ct); if (res.IsSuccessStatusCode) return true; - if (res.StatusCode is HttpStatusCode.Conflict or HttpStatusCode.NotFound) + if (res.StatusCode is HttpStatusCode.Conflict or HttpStatusCode.NotFound or HttpStatusCode.UnprocessableEntity) { + // Conflict/NotFound: contradictory or vanished report — nothing to retry. + // UnprocessableEntity: the server rejected the payload shape itself, so + // resending the same bytes can only ever produce the same 422 — retrying + // just spams the log forever instead of surfacing the real problem. _log.LogWarning("Server permanently rejected {Kind} for job {JobId}: {Status} {Body}", kind, jobId, (int)res.StatusCode, await res.Content.ReadAsStringAsync(ct)); - return true; // don't retry contradictory/vanished reports + return true; } _log.LogWarning("Lifecycle report {Kind} for {JobId} failed with {Status}; will retry", kind, jobId, (int)res.StatusCode); return false; diff --git a/lib/render-pipeline/deliveries.ts b/lib/render-pipeline/deliveries.ts index d3cf0d6..a1d5f7f 100644 --- a/lib/render-pipeline/deliveries.ts +++ b/lib/render-pipeline/deliveries.ts @@ -437,8 +437,8 @@ export const deliveryReportSchema = z.object({ status: z.enum(["COPIED", "VERIFIED", "FAILED"]), exrPath: z.string().max(1000).optional().nullable(), movPath: z.string().max(1000).optional().nullable(), - fileCount: z.number().int().nonnegative().optional(), - totalBytes: z.number().nonnegative().optional(), + fileCount: z.number().int().nonnegative().optional().nullable(), + totalBytes: z.number().nonnegative().optional().nullable(), checksum: z.string().max(200).optional().nullable(), linkMode: z.enum(["hardlink", "copy"]).optional().nullable(), errorMessage: z.string().max(2000).optional().nullable(),