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

  1. Make completion calls idempotent on the client: on 409, GET the file asset and accept the existing state if it is Available.
  2. Disable the submit button / dedupe in-flight completion requests.
  3. Check the asset's current Status before finalizing; only finalize when status == PendingUpload.
  4. 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

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


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)