fullstackhero/dotnet-starter-kit · error · NotFoundException
Invoice not found.
Error message
Invoice {query.InvoiceId} not found. What it means
GetInvoiceByIdQueryHandler looks up the invoice by id and, when the caller is not root, adds a TenantId == callerTenantId filter to the otherwise unfiltered BillingDbContext query. If no invoice matches (wrong id, or the invoice belongs to another tenant), it throws NotFoundException("Invoice {id} not found."). Notably the same 404 is returned for cross-tenant ids so existence is never leaked.
Solutions
- Verify the invoice id exists and matches the intended tenant by querying the invoices table (SELECT * FROM Invoices WHERE Id = '<id>').
- If you are a tenant caller, confirm the invoice actually belongs to your tenant; cross-tenant reads require the root identity.
- Re-check the id source: re-list invoices via GET /invoices and use an id from that response rather than a hand-copied value.
- If root needs cross-tenant visibility, confirm the caller token is root (MultitenancyConstants.Root.Id) — non-root callers are pinned to their own TenantId.
Example fix
// before: stale/hand-copied id
await apiFetch(`/invoices/${invoiceIdFromTicket}`);
// after: resolve a fresh id from the tenant-scoped list
const { items } = await apiFetch('/invoices');
const target = items.find(i => i.invoiceNumber === 'INV-0042');
await apiFetch(`/invoices/${target.id}`); Defensive patterns
Strategy: try-catch
Validate before calling
const res = await apiFetch('/invoices');
const exists = res.items.some(i => i.id === invoiceId);
if (!exists) throw new Error(`Invoice ${invoiceId} not visible to this tenant`); Type guard
function isNotFound(e) { return e?.status === 404 || /not found/i.test(e?.message ?? ''); } Try / catch
try { return await apiFetch(`/invoices/${id}`); }
catch (e) { if (isNotFound(e)) { return null; } throw e; } Prevention
- Derive invoice ids from the tenant-scoped list response, never from hand-copied values or tickets.
- Remember cross-tenant ids return 404 by design — use a root token for cross-tenant lookups.
- Tag ids with their environment when copying between staging and prod to avoid mismatches.
- Handle 404 in the UI with a 'not found or not accessible' message rather than retrying.
When it happens
Trigger: GET /invoices/{id} with an invoice id that does not exist; a tenant caller passing another tenant's invoice id (filtered out by TenantId predicate); a deleted or soft-removed invoice id; root passing a typo'd GUID.
Common situations: Frontend caching an invoice id from a different environment (staging vs prod database); copying an invoice id from a support ticket issued for another tenant; referencing an invoice before the assign/creation transaction committed; case/format-mangled id from a URL.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- Invoice not found.
- Invoice not found.
- 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/944db1db83564ecb.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetInvoiceById/GetInvoiceByIdQueryHandler.cs:33
: 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)