fullstackhero/dotnet-starter-kit · warning · ForbiddenException

not your pending file

Error message

not your pending file

What it means

The pending FileAsset exists but asset.CreatedByUserId does not equal the current user's id (ordinal comparison), so the handler throws ForbiddenException("not your pending file"). Finalization is restricted to the uploader who created the pending record — ownership of the pending upload is captured at init time.

Solutions

  1. Perform the finalize call with the same authenticated user that initiated the upload.
  2. If a backend must finalize on behalf of users, implement a server-side path that carries the original user id rather than reusing the user endpoint.
  3. Do not share presigned upload URLs plus finalize responsibilities across accounts.
  4. Verify the token's user id matches asset.CreatedByUserId before calling finalize.

Example fix

// before
await using (var svc = GetServiceAccountClient()) // service principal ≠ uploader
    await svc.FinalizeUploadAsync(assetId); // 403
// after
await using (var userClient = GetClientForUser(originalUploaderUserId))
    await userClient.FinalizeUploadAsync(assetId);
Defensive patterns

Strategy: validation

Validate before calling

var file = await client.GetFileAsync(assetId);
if (file.CreatedByUserId != currentUserId)
    throw new UnauthorizedAccessException("Only the uploader who initiated this upload may finalize it.");

Try / catch

catch (ForbiddenException e) when (e.Message == "not your pending file") {
    notify("This upload belongs to another user.");
}

Prevention

When it happens

Trigger: A different authenticated user (or a re-issued token for another identity) calls finalize for someone else's pending upload; service account finalizes on behalf of a user; the same human authenticates with a different user id than during init (tenant admin vs member account).

Common situations: Backend job completes the browser upload server-side using a service principal identity; team members sharing presigned URLs then colliding at finalize; user re-login switching identities mid-upload; impersonation sessions changing the effective user.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

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

    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);
            db.FileAssets.Remove(asset);
            await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
            throw new CustomException(
                $"uploaded size ({head.SizeBytes}) exceeds declared ({asset.SizeBytes})",

View on GitHub (pinned to 3f2959e683)