fullstackhero/dotnet-starter-kit · error · UnauthorizedException
Tenant context is required.
Error message
Tenant context is required.
What it means
GetInvoiceByIdQueryHandler resolves the caller's tenant id from the Finbuckle multitenancy context before querying BillingDbContext. Because BillingDbContext is deliberately NOT tenant-filtered, the handler performs its own tenant scoping; if there is no tenant info at all (no tenant resolution middleware match, or a call outside the multitenant pipeline), it throws UnauthorizedException("Tenant context is required.") instead of risking an unscoped cross-tenant read.
Solutions
- Ensure the request carries a resolvable tenant identifier (e.g. ?__tenant__=<id>, X-Tenant-Id header, or a host mapped to a tenant) so Finbuckle populates TenantInfo.
- Verify Finbuckle multitenancy and its resolution strategies are registered in the host pipeline (UseMultiTenancy / strategy configuration) before the endpoint executes.
- If calling from non-HTTP code (jobs/tests), run inside a tenant scope (e.g. ITenantInfo scope / WithTenant) or use a root-level admin path explicitly rather than this tenant-guarded query.
- Check that MultitenancyConstants and the tenant store actually contain the tenant the client is sending; an unknown identifier resolves to null TenantInfo.
Example fix
// before: request without tenant resolution GET /invoices/3f6a... // after: include tenant identifier GET /invoices/3f6a...?__tenant__=acme
Defensive patterns
Strategy: validation
Validate before calling
const tenantId = new URLSearchParams(location.search).get('__tenant__')
?? localStorage.getItem('tenantId');
if (!tenantId) throw new Error('No tenant context; set __tenant__ or X-Tenant-Id before calling Billing APIs');
// then send headers: { 'X-Tenant-Id': tenantId } Type guard
function hasTenant(t) { return typeof t === 'string' && t.length > 0; } Try / catch
try { return await apiFetch(`/invoices/${id}`); }
catch (e) { if (isUnauthorized(e)) { redirectToTenantSelection(); } throw e; } Prevention
- Always send the tenant header/query token on Billing API calls (apiFetch already injects it — don't bypass it).
- Register every deployment hostname as a tenant in the tenant store so host-based resolution works.
- In tests/jobs, wrap handler calls in an explicit tenant scope instead of relying on HTTP middleware.
- Check reverse-proxy config preserves Host/X-Forwarded-Host headers used by the tenant strategy.
When it happens
Trigger: Calling GET /invoices/{id} (v1) without a resolvable tenant: no __tenant__ query/header/route token, no tenant-matched host, or the request bypassed Finbuckle's MultiTenantMiddleware / strategy resolution so MultiTenantContext.TenantInfo is null.
Common situations: Hitting the endpoint from a background job, integration test, or console seed code that constructs the handler without going through the tenant middleware; a misconfigured host-header strategy where the deployment hostname is not a registered tenant; calling a tenant-scoped endpoint with an admin token that was never bound to a tenant.
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/38a359fde16af2f2.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetInvoiceById/GetInvoiceByIdQueryHandler.cs:24
using FSH.Modules.Billing.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Billing.Features.v1.Invoices.GetInvoiceById;
public sealed class GetInvoiceByIdQueryHandler(
BillingDbContext dbContext,
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: IQueryHandler<GetInvoiceByIdQuery, InvoiceDto>
{
public async ValueTask<InvoiceDto> Handle(GetInvoiceByIdQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
// BillingDbContext isn't tenant-filtered (raw DbContext for cross-tenant admin visibility): root
// reads any invoice by id; a tenant caller is pinned to its own so it can't read another's. Mirrors GetSubscriptionQueryHandler.
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
var isRoot = callerTenantId == MultitenancyConstants.Root.Id;
var invoice = await dbContext.Invoices.AsNoTracking()
.Include(i => i.LineItems)
.FirstOrDefaultAsync(
i => i.Id == query.InvoiceId && (isRoot || i.TenantId == callerTenantId),
cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Invoice {query.InvoiceId} not found.");
return invoice.ToDto();
}
}
View on GitHub (pinned to 3f2959e683)