fullstackhero/dotnet-starter-kit · error · CustomException
Cannot finalize file in status
Error message
Cannot finalize file in status {Status}. What it means
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.
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.
Example fix
// before
await api.post(`/files/${id}/complete`); // 409 if already completed
// after
const asset = await api.get(`/files/${id}`);
if (asset.status === 'PendingUpload') await api.post(`/files/${id}/complete`); // else already finalized Defensive patterns
Strategy: try-catch
Validate before calling
const asset = await api.get(`/files/${id}`);
if (asset.status !== 'PendingUpload') { skipFinalize(asset); return; } Type guard
const canFinalize = (a: FileAsset): a is FileAsset & { status: 'PendingUpload' } => a.status === 'PendingUpload'; Try / catch
try { await api.post(`/files/${id}/complete`); }
catch (e) { if (isConflict(e)) { const a = await api.get(`/files/${id}`); reconcileWith(a); } else { throw e; } } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot change visibility while file is in status
- upload not received
- Top-up request cannot be rejected because it is (only…
- Declared size must be positive.
- Actual size must be positive.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/1d614fb681b6b1a8.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Files/Modules.Files/Domain/FileAsset.cs:87
OriginalFileName = originalFileName,
FileName = sanitizedFileName,
ContentType = contentType,
SizeBytes = declaredSizeBytes,
StorageKey = storageKey,
Visibility = visibility,
Status = FileAssetStatus.PendingUpload,
ScanStatus = ScanStatus.NotScanned,
UploadDeadline = uploadDeadline,
CreatedByUserId = createdByUserId,
CreatedAtUtc = DateTime.UtcNow
};
}
public void MarkAvailable(long actualSize, ScanStatus scanResult)
{
if (Status != FileAssetStatus.PendingUpload)
{
throw new CustomException(
$"Cannot finalize file in status {Status}.",
errors: null,
HttpStatusCode.Conflict);
}
if (actualSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(actualSize), "Actual size must be positive.");
}
SizeBytes = actualSize;
ScanStatus = scanResult;
Status = scanResult == ScanStatus.Infected ? FileAssetStatus.Quarantined : FileAssetStatus.Available;
UploadDeadline = null;
UpdatedAtUtc = DateTime.UtcNow;
AddDomainEvent(DomainEvent.Create((id, ts) =>
new FileFinalizedDomainEvent(Id, OwnerType, OwnerId, Status, id, ts)));
}View on GitHub (pinned to 3f2959e683)