fullstackhero/dotnet-starter-kit · error · UnauthorizedException
Tenant context is required.
Error message
Tenant context is required.
What it means
This handler throws UnauthorizedException('Tenant context is required.') when tenantAccessor.MultiTenantContext?.TenantInfo?.Id resolves to null. BillingDbContext is not tenant-filtered, so the handler must resolve the caller's tenant id explicitly to pin reads to that tenant; if Finbuckle has not resolved a tenant for the request, no safe tenant scope exists and the handler refuses to run. It is an application-level guard, not an infrastructure failure.
Solutions
- Pass a tenant identifier with the request: add header __tenant__: <tenant-id-or-identifier> (or use the tenant-resolving hostname/route value configured in the app).
- Verify Finbuckle multitenancy middleware is registered and ordered before endpoint mapping in the host pipeline.
- If calling from a non-HTTP context (job/CLI), set the tenant explicitly (e.g. ITenantContext/TenantInfo scoped service or tenant context accessor) before invoking the handler.
- In tests, register/configure a mock ITenantAccessor whose MultiTenantContext.TenantInfo.Id returns a non-null tenant id.
Example fix
// before curl -H "Authorization: Bearer $TOKEN" https://localhost:7030/api/v1/subscriptions/3fa85f64... // after curl -H "Authorization: Bearer $TOKEN" -H "__tenant__: acme" https://localhost:7030/api/v1/subscriptions/3fa85f64...
Defensive patterns
Strategy: try-catch
Validate before calling
var tenantId = request.Headers.TryGetValues("__tenant__", out var v) ? v.FirstOrDefault() : null;
if (string.IsNullOrWhiteSpace(tenantId)) throw new InvalidOperationException("__tenant__ header required before calling GetSubscription"); Type guard
bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;
Try / catch
try
{
var sub = await api.GetSubscriptionAsync(id);
}
catch (UnauthorizedException ex) when (ex.Message == "Tenant context is required.")
{
logger.LogWarning(ex, "No tenant context resolved; re-issue with __tenant__ header");
// re-dispatch with tenant header or surface a 401 to the caller
} Prevention
- Always attach the __tenant__ header (or use the tenant's mapped host) in API clients and test fixtures.
- Add an integration-test asserting every Billing endpoint returns 401 (not 500) when tenant context is missing.
- Never call Billing handlers from background jobs without entering a tenant scope first.
- Keep multitenancy middleware registered before UseEndpoints/MapEndpoints.
When it happens
Trigger: Calling GET subscription endpoint without a tenant identifier resolvable by Finbuckle — e.g. request missing the __tenant__ header/route/query value, hostname not mapped to a tenant in TenantStore, or the call made from a background/Hangfire/CLI context with no TenantContext set.
Common situations: Testing the endpoint with curl/Postman omitting the tenant header; a reverse proxy stripping the tenant host header; calling the API as root/admin without impersonating a tenant; integration tests that build the handler manually without seeding ITenantInfo; middleware ordering where multitenancy middleware runs after the endpoint pipeline.
Related errors
- Tenant context is required.
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
- Tenant context is required.
- Tenant context is required.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/56c4b78055d2f9c6.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Subscriptions/GetSubscription/GetSubscriptionQueryHandler.cs:22
using FSH.Modules.Billing.Contracts.Dtos;
using FSH.Modules.Billing.Contracts.v1.Subscriptions;
using FSH.Modules.Billing.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Billing.Features.v1.Subscriptions.GetSubscription;
public sealed class GetSubscriptionQueryHandler(
BillingDbContext dbContext,
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: IQueryHandler<GetSubscriptionQuery, SubscriptionDto?>
{
public async ValueTask<SubscriptionDto?> Handle(GetSubscriptionQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
// BillingDbContext is not tenant-filtered, so a tenant caller is pinned to its OWN
// subscription and only root may pass an arbitrary tenant id (else cross-tenant reads).
var tenantId = callerTenantId == MultitenancyConstants.Root.Id
? query.TenantId ?? callerTenantId
: callerTenantId;
var sub = await (from s in dbContext.Subscriptions.AsNoTracking()
join p in dbContext.Plans.AsNoTracking() on s.PlanId equals p.Id
where s.TenantId == tenantId
&& s.Status == Contracts.SubscriptionStatus.Active
select new SubscriptionDto(s.Id, s.TenantId, s.PlanId, p.Key, s.StartUtc, s.EndUtc, s.Status))
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return sub;
}
}
View on GitHub (pinned to 3f2959e683)