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
- Perform the finalize call with the same authenticated user that initiated the upload.
- 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.
- Do not share presigned upload URLs plus finalize responsibilities across accounts.
- 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
- Keep the whole upload lifecycle (init → PUT → finalize) under one authenticated identity.
- Do not finalize on behalf of users with a service principal; add a server-side path if needed.
- Avoid sharing presigned URLs across accounts.
- Detect identity changes (re-login/impersonation) mid-upload and restart the flow.
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
- Only the author can edit a message.
- Channel admin role required.
- not allowed to change this file's visibility
- not allowed to delete this file
- Only administrators can change user status.
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)