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
- Send the approve request with the tenant identifier set (header __tenant__ or mapped tenant host).
- Verify Finbuckle middleware registration/order in the API host.
- For approving other tenants' requests, use root credentials with root tenant context resolved.
- 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
- Centralize tenant header injection in the API client used by admin tooling.
- Forbid manual handler construction in tests without a tenant-aware ITenantAccessor.
- Audit Hangfire/CLI callers of wallet handlers for tenant scopes.
- Verify tenant resolution after any reverse-proxy or host-header change.
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
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
- Tenant context is required.
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)