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(),