{"record":{"id":"19c3d2ff16ec0161","repo":"fullstackhero/dotnet-starter-kit","slug":"file-not-found-finalizeuploadcommandhandler","errorCode":null,"errorMessage":"file not found","messagePattern":"file not found","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs","lineNumber":39,"sourceCode":"public sealed class FinalizeUploadCommandHandler(\n    FilesDbContext db,\n    IStorageService storage,\n    IFileScanner scanner,\n    IQuotaService quotas,\n    IOutboxWriter outbox,\n    ICurrentUser currentUser)\n    : ICommandHandler<FinalizeUploadCommand, FileAssetDto>\n{\n    public async ValueTask<FileAssetDto> Handle(FinalizeUploadCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        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);","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs#L21-L57","documentation":"The FileAsset lookup by cmd.FileAssetId found no row, so the handler throws NotFoundException. Finalize operates on the PendingUpload row created by the earlier InitiateUpload step; because the query is tenant-filtered, cross-tenant or soft-deleted ids also surface as 'file not found'.","triggerScenarios":"Finalize called with an id from an init step that never persisted (init failed/rolled back), an id already finalized-and-purged, a wrong-tenant id, or a client-supplied fabricated GUID.","commonSituations":"Client retries finalize after init errored mid-flight; the pending row expired/was purged by a cleanup job; environments switched between init and finalize; copy/paste of ids across tenants during testing.","solutions":["Re-run the upload initialization step and use the FileAssetId it returns; never hand-construct ids.","Confirm init persisted successfully (no rollback/timeout) before calling finalize.","Ensure the same tenant context is used for init and finalize.","Check whether a cleanup/purge job removed the pending asset; restart the upload flow if so."],"exampleFix":"// before\nvar id = Guid.NewGuid();\nawait client.FinalizeUploadAsync(id); // 404\n// after\nvar init = await client.InitiateUploadAsync(fileName, sizeBytes, contentType);\nawait uploadToPresignedUrl(init.Url, file);\nawait client.FinalizeUploadAsync(init.FileAssetId);","handlingStrategy":"validation","validationCode":"if (initResult?.FileAssetId is not Guid id || id == Guid.Empty)\n    throw new InvalidOperationException(\"Run the upload initialization step first; finalize requires its returned FileAssetId.\");","typeGuard":"bool HasPendingUpload(InitiateUploadResponse? r) => r?.FileAssetId is Guid id && id != Guid.Empty;","tryCatchPattern":"catch (NotFoundException e) when (e.Message == \"file not found\") {\n    await RestartUploadFlowAsync(); // re-init and re-upload\n}","preventionTips":["Always chain finalize from the init response in the same session/tenant.","Check init succeeded before starting the binary upload.","Be aware pending rows can be purged by cleanup jobs — restart the flow rather than retrying finalize.","Never hard-code or hand-build FileAssetIds in scripts."],"tags":["not-found","files","upload","tenancy"],"backgroundTag":"entity-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"}