fullstackhero/dotnet-starter-kit · error · CustomException

upload not received

Error message

upload not received

What it means

FinalizeUploadCommandHandler verifies with the storage provider (S3/MinIO) that the client actually uploaded bytes to the presigned location before marking the FileAsset finalized. HeadObjectAsync returned null, meaning no object exists at asset.StorageKey, so the handler throws CustomException with HTTP 409 Conflict and the asset stays in PendingUpload.

Solutions

  1. Ensure the client awaits its upload PUT/multipart-completion before calling finalize-upload
  2. Retry the finalize endpoint after confirming the upload completed (the asset is still PendingUpload, so a later finalize can succeed)
  3. Re-run the upload flow from the start if the object cannot be found in the bucket
  4. Check bucket lifecycle/expiration rules in S3/MinIO that may remove objects before finalize

Example fix

// before (client)
await api.finalizeUpload(assetId); // fired before upload
// after (client)
await uploadToPresignedUrl(asset.uploadUrl, file);
await api.finalizeUpload(assetId);
Defensive patterns

Strategy: retry

Validate before calling

// client: confirm object exists before finalize
const head = await headPresignedObject(asset.storageKey);
if (!head) throw new Error('upload incomplete; do not finalize');

Try / catch

try { await api.finalizeUpload(assetId); }
catch (e) { if (e.status === 409 && e.message === 'upload not received') { await redoUpload(); } else throw e; }

Prevention

When it happens

Trigger: Calling the finalize-upload endpoint before the client completed its PUT/multipart upload to the presigned URL; uploading to the wrong storage key; the object was deleted between upload and finalize; an aborted multipart upload with no parts.

Common situations: Client fires finalize in parallel with the upload instead of awaiting it; presigned URL expired mid-transfer and the client silently skipped the PUT; MinIO/S3 bucket lifecycle rules purged the object; network drop causes the uploader to skip error handling and proceed to finalize.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/6bba3937bb311b92. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs:51

        var tenantId = currentUser.GetTenant() ?? throw new UnauthorizedException("invalid tenant");
        var userId = currentUser.GetUserId().ToString();

        var asset = await db.FileAssets
            .FirstOrDefaultAsync(f => f.Id == cmd.FileAssetId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException("file not found");

        if (!string.Equals(asset.CreatedByUserId, userId, StringComparison.Ordinal))
        {
            throw new ForbiddenException("not your pending file");
        }
        if (asset.Status != FileAssetStatus.PendingUpload)
        {
            throw new CustomException("file already finalized", (IEnumerable<string>?)null, HttpStatusCode.Conflict);
        }

        var head = await storage.HeadObjectAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false)
            ?? throw new CustomException("upload not received", (IEnumerable<string>?)null, HttpStatusCode.Conflict);

        // Allow declared+1% slack (S3 may differ slightly on multipart). Reject larger sizes.
        var maxAllowed = asset.SizeBytes + Math.Max(1024L, asset.SizeBytes / 100);
        if (head.SizeBytes > maxAllowed)
        {
            await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);
            db.FileAssets.Remove(asset);
            await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
            throw new CustomException(
                $"uploaded size ({head.SizeBytes}) exceeds declared ({asset.SizeBytes})",
                (IEnumerable<string>?)null,
                HttpStatusCode.BadRequest);
        }

        if (!string.Equals(head.ContentType, asset.ContentType, StringComparison.OrdinalIgnoreCase))
        {
            await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);
            db.FileAssets.Remove(asset);

View on GitHub (pinned to 3f2959e683)