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

  1. Re-run the upload initialization step and use the FileAssetId it returns; never hand-construct ids.
  2. Confirm init persisted successfully (no rollback/timeout) before calling finalize.
  3. Ensure the same tenant context is used for init and finalize.
  4. 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

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


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)