fullstackhero/dotnet-starter-kit · error · UnauthorizedException
invalid tenant
Error message
invalid tenant
What it means
FinalizeUploadCommandHandler requires an authenticated tenant context: currentUser.GetTenant() returns null when the request has no tenant claim/binding, and the handler throws UnauthorizedException("invalid tenant"). Finbuckle multitenancy plus the JWT tenant claim feed ICurrentUser; without them the upload cannot be attributed or quota-charged.
Solutions
- Send the tenant identifier with the request (tenant header/route per the configured Finbuckle strategy).
- Ensure the JWT contains the tenant claim when issued, or re-authenticate after fixing token issuance.
- Verify the endpoint is not reachable anonymously; require authentication for finalize-upload.
- For non-interactive callers, use a properly tenant-scoped service credential rather than calling user endpoints.
Example fix
// before
fetch('/api/v1/files/finalize', { method: 'POST', ... }); // no tenant
// after
fetch('/api/v1/files/finalize', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'tenant': tenantId }, ... }); Defensive patterns
Strategy: type-guard
Validate before calling
if (string.IsNullOrWhiteSpace(tenantId)) throw new InvalidOperationException("Call finalize-upload with a tenant identifier (header/claim) set."); Type guard
bool HasTenantContext(ICurrentUser user) => user.GetTenant() is not null;
Try / catch
catch (UnauthorizedException e) when (e.Message == "invalid tenant") {
redirectToTenantSelection(); // or attach tenant header and retry once
} Prevention
- Always send the tenant header/segment your Finbuckle strategy expects.
- Ensure the identity server includes the tenant claim in tokens.
- Require authentication on finalize-upload; reject anonymous calls early.
- For service-to-service calls, use tenant-scoped credentials, not user endpoints.
When it happens
Trigger: Calling the finalize-upload endpoint without a tenant identifier (missing tenant header/route segment, missing 'tenant' claim in the JWT, anonymous or tenant-less service token), or the tenant resolver failing to match the identifier.
Common situations: Direct API/script calls that omit the tenant header the UI normally sends; tokens issued by a legacy identity setup without tenant claims; Finbuckle strategy misconfigured (e.g. expected __tenant__ query/header absent); background jobs calling the handler without tenant context.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/281947e6ff5f1d95.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs:33
using FSH.Modules.Files.Services;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Files.Features.v1.FinalizeUpload;
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);View on GitHub (pinned to 3f2959e683)