fullstackhero/dotnet-starter-kit · error · NotFoundException
file not found
Error message
file not found
What it means
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'.
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.
Example fix
// before var id = Guid.NewGuid(); await client.FinalizeUploadAsync(id); // 404 // after var init = await client.InitiateUploadAsync(fileName, sizeBytes, contentType); await uploadToPresignedUrl(init.Url, file); await client.FinalizeUploadAsync(init.FileAssetId);
Defensive patterns
Strategy: validation
Validate before calling
if (initResult?.FileAssetId is not Guid id || id == Guid.Empty)
throw new InvalidOperationException("Run the upload initialization step first; finalize requires its returned FileAssetId."); Type guard
bool HasPendingUpload(InitiateUploadResponse? r) => r?.FileAssetId is Guid id && id != Guid.Empty;
Try / catch
catch (NotFoundException e) when (e.Message == "file not found") {
await RestartUploadFlowAsync(); // re-init and re-upload
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
- file not found
- Declared size must be positive.
- Cannot finalize file in status
- Actual size must be positive.
- file not found
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/19c3d2ff16ec0161.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs:39
public sealed class FinalizeUploadCommandHandler(
FilesDbContext db,
IStorageService storage,
IFileScanner scanner,
IQuotaService quotas,
IOutboxWriter outbox,
ICurrentUser currentUser)
: ICommandHandler<FinalizeUploadCommand, FileAssetDto>
{
public async ValueTask<FileAssetDto> Handle(FinalizeUploadCommand cmd, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(cmd);
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);View on GitHub (pinned to 3f2959e683)