fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Tenant context is required.

Error message

Tenant context is required.

What it means

ApproveTopupRequestCommandHandler needs the caller's tenant id to enforce that only root may approve requests of other tenants and that non-root callers approve only their own. It reads tenantAccessor.MultiTenantContext?.TenantInfo?.Id and throws UnauthorizedException('Tenant context is required.') when it is null, since without a tenant the ownership check is impossible.

Solutions

  1. Send the approve request with the tenant identifier set (header __tenant__ or mapped tenant host).
  2. Verify Finbuckle middleware registration/order in the API host.
  3. For approving other tenants' requests, use root credentials with root tenant context resolved.
  4. In tests, mock ITenantAccessor to return a non-null TenantInfo.Id.

Example fix

// before
await client.PostAsync($"/api/v1/wallets/topup-requests/{id}/approve", null);
// after
var req = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/wallets/topup-requests/{id}/approve");
req.Headers.Add("__tenant__", "acme");
await client.SendAsync(req);
Defensive patterns

Strategy: try-catch

Validate before calling

if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)
    throw new InvalidOperationException("ApproveTopupRequest requires tenant context; set __tenant__ header.");

Type guard

bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;

Try / catch

try { await api.ApproveTopupRequestAsync(id); }
catch (UnauthorizedException ex) when (ex.Message == "Tenant context is required.")
{
    // add tenant header and retry once
}

Prevention

When it happens

Trigger: POSTing an approve-top-up-request command without a Finbuckle-resolvable tenant (missing __tenant__ header/host mapping), or calling the handler from Hangfire/CLI without a tenant context.

Common situations: Approving from an admin tool that omits the tenant header; host-header rewriting by a proxy breaking tenant resolution; tests instantiating the handler with an unconfigured tenant accessor.

Related errors


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

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/ApproveTopupRequest/ApproveTopupRequestCommandHandler.cs:23

using FSH.Modules.Billing.Data;
using FSH.Modules.Billing.Services;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace FSH.Modules.Billing.Features.v1.Wallets.ApproveTopupRequest;

public sealed class ApproveTopupRequestCommandHandler(
    BillingDbContext db,
    IBillingService billing,
    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : ICommandHandler<ApproveTopupRequestCommand, Guid>
{
    public async ValueTask<Guid> Handle(ApproveTopupRequestCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
            ?? throw new UnauthorizedException("Tenant context is required.");
        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;

        var request = await db.TopupRequests
            .FirstOrDefaultAsync(r => r.Id == command.Id, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException($"Top-up request {command.Id} not found.");

        if (!isRoot && request.TenantId != callerTenantId)
        {
            throw new UnauthorizedException("You can only approve top-up requests for your own tenant.");
        }

        // For root, operate on the request's own tenant; for non-root, callerTenantId equals request.TenantId.
        var invoice = await billing.CreateTopupInvoiceAsync(request.TenantId, command.Id, cancellationToken)
            .ConfigureAwait(false);

        return invoice.Id;
    }

View on GitHub (pinned to 3f2959e683)