{"record":{"id":"1d614fb681b6b1a8","repo":"fullstackhero/dotnet-starter-kit","slug":"cannot-finalize-file-in-status-status","errorCode":null,"errorMessage":"Cannot finalize file in status {Status}.","messagePattern":"Cannot finalize file in status (.+?)\\.","errorType":"exception","errorClass":"CustomException","httpStatus":409,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Domain/FileAsset.cs","lineNumber":87,"sourceCode":"            OriginalFileName = originalFileName,\n            FileName = sanitizedFileName,\n            ContentType = contentType,\n            SizeBytes = declaredSizeBytes,\n            StorageKey = storageKey,\n            Visibility = visibility,\n            Status = FileAssetStatus.PendingUpload,\n            ScanStatus = ScanStatus.NotScanned,\n            UploadDeadline = uploadDeadline,\n            CreatedByUserId = createdByUserId,\n            CreatedAtUtc = DateTime.UtcNow\n        };\n    }\n\n    public void MarkAvailable(long actualSize, ScanStatus scanResult)\n    {\n        if (Status != FileAssetStatus.PendingUpload)\n        {\n            throw new CustomException(\n                $\"Cannot finalize file in status {Status}.\",\n                errors: null,\n                HttpStatusCode.Conflict);\n        }\n        if (actualSize <= 0)\n        {\n            throw new ArgumentOutOfRangeException(nameof(actualSize), \"Actual size must be positive.\");\n        }\n\n        SizeBytes = actualSize;\n        ScanStatus = scanResult;\n        Status = scanResult == ScanStatus.Infected ? FileAssetStatus.Quarantined : FileAssetStatus.Available;\n        UploadDeadline = null;\n        UpdatedAtUtc = DateTime.UtcNow;\n\n        AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new FileFinalizedDomainEvent(Id, OwnerType, OwnerId, Status, id, ts)));\n    }","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Domain/FileAsset.cs#L69-L105","documentation":"FileAsset.MarkAvailable can only be called while the asset is in PendingUpload status; otherwise it throws CustomException with 409 Conflict. It finalizes the upload (sets actual size, scan result, and Available/Quarantined status), so calling it on an already-finalized, quarantined or deleted file is a state-machine violation.","triggerScenarios":"Calling the finalize/complete-upload endpoint twice (double submit or retry); finalizing after the file was quarantined (Infected scan) or otherwise transitioned out of PendingUpload; the upload deadline passed and the asset was cleaned up.","commonSituations":"Client retry logic re-POSTs the completion call after a network timeout although the first call succeeded; Hangfire cleanup job moved the asset out of PendingUpload; user re-uploads over the same file id.","solutions":["Make completion calls idempotent on the client: on 409, GET the file asset and accept the existing state if it is Available.","Disable the submit button / dedupe in-flight completion requests.","Check the asset's current Status before finalizing; only finalize when status == PendingUpload.","Re-initiate the upload (new CreatePending + presigned PUT) if the asset is no longer pending."],"exampleFix":"// before\nawait api.post(`/files/${id}/complete`); // 409 if already completed\n// after\nconst asset = await api.get(`/files/${id}`);\nif (asset.status === 'PendingUpload') await api.post(`/files/${id}/complete`); // else already finalized","handlingStrategy":"try-catch","validationCode":"const asset = await api.get(`/files/${id}`);\nif (asset.status !== 'PendingUpload') { skipFinalize(asset); return; }","typeGuard":"const canFinalize = (a: FileAsset): a is FileAsset & { status: 'PendingUpload' } => a.status === 'PendingUpload';","tryCatchPattern":"try { await api.post(`/files/${id}/complete`); }\ncatch (e) { if (isConflict(e)) { const a = await api.get(`/files/${id}`); reconcileWith(a); } else { throw e; } }","preventionTips":["Dedupe in-flight completion requests (single-flight lock)","Treat 409 on finalize as 'already done' and refetch state","Only allow finalization while the upload deadline is live"],"tags":["files","state-machine","conflict","upload"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}