fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Tenant context is required.

Error message

Tenant context is required.

What it means

GenerateInvoicesCommandHandler throws UnauthorizedException when the caller has no tenant context (MultiTenantContext/TenantInfo is null). Platform-wide invoice generation is a root-operator action, so an anonymous or non-tenant-resolved caller is rejected first.

Solutions

  1. Send the tenant identifier (X-Tenant header or tenant host) with the request.
  2. Verify Finbuckle tenant resolution strategy and configured tenant mappings.
  3. For automation, use the root tenant context explicitly (MultitenancyConstants.Root.Id).
  4. If the endpoint should be non-tenant, move it to a root-scoped route with proper resolver config.

Example fix

// before
await http.PostAsync("/api/v1/invoices/generate", content); // no tenant header

// after
request.Headers.Add("X-Tenant", "root");
await http.SendAsync(request);
Defensive patterns

Strategy: validation

Validate before calling

var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id; if (string.IsNullOrEmpty(tenantId)) throw new UnauthorizedException("Tenant context is required.");

Try / catch

try { await api.GenerateInvoices(cmd); } catch (UnauthorizedAccessException) { // attach tenant header and retry once }

Prevention

When it happens

Trigger: Calling the generate-invoices endpoint without a valid tenant header/host so Finbuckle cannot resolve TenantInfo; calling from a background job or CLI without tenant context configured.

Common situations: Missing X-Tenant header in API calls; misconfigured tenant resolver for custom domains; invoking the endpoint from a Hangfire job or service client that doesn't set tenant context.

Related errors


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

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GenerateInvoices/GenerateInvoicesCommandHandler.cs:22

using FSH.Modules.Billing.Contracts.v1.Invoices;
using FSH.Modules.Billing.Services;
using Mediator;

namespace FSH.Modules.Billing.Features.v1.Invoices.GenerateInvoices;

public sealed class GenerateInvoicesCommandHandler(
    IBillingService billing,
    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : ICommandHandler<GenerateInvoicesCommand, int>
{
    public async ValueTask<int> Handle(GenerateInvoicesCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        // Platform-wide invoice generation runs across EVERY tenant — it is a root-operator action.
        // A tenant admin (who also holds Billing.Manage) must not be able to trigger it.
        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
            ?? throw new UnauthorizedException("Tenant context is required.");
        if (callerTenantId != MultitenancyConstants.Root.Id)
        {
            throw new ForbiddenException("Only the root operator may generate invoices across tenants.");
        }

        return await billing.GenerateInvoicesForAllTenantsAsync(command.PeriodYear, command.PeriodMonth, cancellationToken).ConfigureAwait(false);
    }
}

View on GitHub (pinned to 3f2959e683)