{"record":{"id":"6bba3937bb311b92","repo":"fullstackhero/dotnet-starter-kit","slug":"upload-not-received","errorCode":null,"errorMessage":"upload not received","messagePattern":"upload not received","errorType":"exception","errorClass":"CustomException","httpStatus":409,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs","lineNumber":51,"sourceCode":"        var tenantId = currentUser.GetTenant() ?? throw new UnauthorizedException(\"invalid tenant\");\n        var userId = currentUser.GetUserId().ToString();\n\n        var asset = await db.FileAssets\n            .FirstOrDefaultAsync(f => f.Id == cmd.FileAssetId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"file not found\");\n\n        if (!string.Equals(asset.CreatedByUserId, userId, StringComparison.Ordinal))\n        {\n            throw new ForbiddenException(\"not your pending file\");\n        }\n        if (asset.Status != FileAssetStatus.PendingUpload)\n        {\n            throw new CustomException(\"file already finalized\", (IEnumerable<string>?)null, HttpStatusCode.Conflict);\n        }\n\n        var head = await storage.HeadObjectAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false)\n            ?? throw new CustomException(\"upload not received\", (IEnumerable<string>?)null, HttpStatusCode.Conflict);\n\n        // Allow declared+1% slack (S3 may differ slightly on multipart). Reject larger sizes.\n        var maxAllowed = asset.SizeBytes + Math.Max(1024L, asset.SizeBytes / 100);\n        if (head.SizeBytes > maxAllowed)\n        {\n            await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);\n            db.FileAssets.Remove(asset);\n            await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n            throw new CustomException(\n                $\"uploaded size ({head.SizeBytes}) exceeds declared ({asset.SizeBytes})\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.BadRequest);\n        }\n\n        if (!string.Equals(head.ContentType, asset.ContentType, StringComparison.OrdinalIgnoreCase))\n        {\n            await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);\n            db.FileAssets.Remove(asset);","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs#L33-L69","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the client awaits its upload PUT/multipart-completion before calling finalize-upload","Retry the finalize endpoint after confirming the upload completed (the asset is still PendingUpload, so a later finalize can succeed)","Re-run the upload flow from the start if the object cannot be found in the bucket","Check bucket lifecycle/expiration rules in S3/MinIO that may remove objects before finalize"],"exampleFix":"// before (client)\nawait api.finalizeUpload(assetId); // fired before upload\n// after (client)\nawait uploadToPresignedUrl(asset.uploadUrl, file);\nawait api.finalizeUpload(assetId);","handlingStrategy":"retry","validationCode":"// client: confirm object exists before finalize\nconst head = await headPresignedObject(asset.storageKey);\nif (!head) throw new Error('upload incomplete; do not finalize');","typeGuard":null,"tryCatchPattern":"try { await api.finalizeUpload(assetId); }\ncatch (e) { if (e.status === 409 && e.message === 'upload not received') { await redoUpload(); } else throw e; }","preventionTips":["Await the upload PUT/multipart completion before calling finalize","Handle upload promise rejections instead of proceeding to finalize","Watch for presigned URL expiry during long uploads"],"tags":["s3","upload","files","conflict"],"backgroundTag":"file-not-found","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"}