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

  1. Send the tenant identifier with the request (tenant header/route per the configured Finbuckle strategy).
  2. Ensure the JWT contains the tenant claim when issued, or re-authenticate after fixing token issuance.
  3. Verify the endpoint is not reachable anonymously; require authentication for finalize-upload.
  4. 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

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)