fullstackhero/dotnet-starter-kit · error · UnauthorizedException
Tenant context is required.
Error message
Tenant context is required.
What it means
GetMyInvoicesQueryHandler always scopes to the caller's own tenant: it reads tenantAccessor.MultiTenantContext.TenantInfo.Id and throws UnauthorizedException("Tenant context is required.") if no tenant is resolved. There is no root bypass here by design — this endpoint is 'my invoices' only.
Solutions
- Resolve a tenant for the caller (tenant header/query token or host mapping) before calling the endpoint.
- If the intent is a cross-tenant listing, switch to GET /invoices with a root token and optional query.TenantId.
- For background execution, run inside an explicit tenant scope.
- Verify tenant middleware ordering so TenantInfo is populated before the query handler runs.
Example fix
// before: root calling 'my' endpoint GET /invoices/my // after: root cross-tenant listing GET /invoices?tenantId=acme
Defensive patterns
Strategy: validation
Validate before calling
if (!tenantId) throw new Error('GetMyInvoices requires a tenant context; resolve one first'); Type guard
function hasTenant(t) { return typeof t === 'string' && t.length > 0; } Try / catch
try { return await apiFetch('/invoices/my'); }
catch (e) { if (isUnauthorized(e)) { redirectToTenantSelection(); return []; } throw e; } Prevention
- Use /invoices/my only from tenant-scoped clients; admins should use GET /invoices instead.
- Resolve tenant context at app bootstrap and fail fast if absent.
- Avoid calling this handler directly from jobs/tests without a tenant scope.
When it happens
Trigger: GET /invoices/my (v1) invoked without tenant resolution: missing tenant token/header, unregistered host, request outside Finbuckle middleware, or a call from non-HTTP code with no tenant scope.
Common situations: A root/admin dashboard reusing the tenant-facing 'my invoices' endpoint without a tenant binding; SignalR/SSE or scheduled export jobs calling the handler directly; test harnesses constructing the query handler without tenant setup.
Related errors
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
- Tenant context is required.
- 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/8ff365d51df22362.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetMyInvoices/GetMyInvoicesQueryHandler.cs:23
using FSH.Modules.Billing.Contracts.Dtos;
using FSH.Modules.Billing.Contracts.v1.Invoices;
using FSH.Modules.Billing.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Billing.Features.v1.Invoices.GetMyInvoices;
public sealed class GetMyInvoicesQueryHandler(
BillingDbContext dbContext,
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: IQueryHandler<GetMyInvoicesQuery, PagedResponse<InvoiceDto>>
{
public async ValueTask<PagedResponse<InvoiceDto>> Handle(GetMyInvoicesQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
var q = dbContext.Invoices.AsNoTracking()
.Include(i => i.LineItems)
.Where(i => i.TenantId == tenantId);
if (query.Status is not null)
{
q = q.Where(i => i.Status == query.Status);
}
if (query.PeriodYear is not null)
{
q = q.Where(i => i.PeriodYear == query.PeriodYear);
}
if (query.PeriodMonth is not null)
{
q = q.Where(i => i.PeriodMonth == query.PeriodMonth);
}
var total = await q.LongCountAsync(cancellationToken).ConfigureAwait(false);View on GitHub (pinned to 3f2959e683)