fullstackhero/dotnet-starter-kit · error · NotFoundException
Invoice not found.
Error message
Invoice {query.InvoiceId} not found. What it means
GetInvoicePdfQueryHandler fetches the invoice (tenant-pinned for non-root callers) before rendering the PDF; if no invoice matches the id within the allowed tenant scope it throws NotFoundException("Invoice {id} not found."). Cross-tenant ids intentionally produce the same 404.
Solutions
- Verify the invoice id exists in the Invoices table and, for non-root callers, that its TenantId matches the caller.
- Re-fetch the invoice list (GET /invoices) and derive the PDF link from a current DTO instead of a stored URL.
- For cross-tenant PDF access, authenticate as root; otherwise the TenantId filter will yield a 404.
- Check for environment mismatch (staging id used against production API).
Example fix
// before: id from an old email link
GET /invoices/9d2e.../pdf
// after: resolve from list first
const invoices = await apiFetch('/invoices');
const inv = invoices.items.find(i => i.invoiceNumber === 'INV-0042');
return apiFetch(`/invoices/${inv.id}/pdf`); Defensive patterns
Strategy: try-catch
Validate before calling
const invoices = await apiFetch('/invoices');
if (!invoices.items.some(i => i.id === invoiceId)) return null; // PDF would 404 too Type guard
function isNotFound(e) { return e?.status === 404 || /not found/i.test(e?.message ?? ''); } Try / catch
try { return await apiFetch(`/invoices/${id}/pdf`); }
catch (e) { if (isNotFound(e)) { show('Invoice not found or not accessible'); return null; } throw e; } Prevention
- Build PDF links from freshly fetched invoice DTOs, not stored URLs.
- Check environment alignment: an id valid in staging will 404 in prod.
- Use a root token when cross-tenant PDF access is legitimately required.
When it happens
Trigger: GET /invoices/{id}/pdf with a non-existent invoice id; a tenant caller requesting another tenant's invoice id; an id valid in another environment/database; a deleted invoice id.
Common situations: A stale dashboard link to an invoice that was re-created (new id) after a plan reassignment; downloading a PDF for an invoice created in staging while pointing at prod; sharing a PDF URL between tenants.
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.
- Tenant context is required.
- Invoice not found.
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/c1410819e3f12cee.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetInvoicePdf/GetInvoicePdfQueryHandler.cs:33
: IQueryHandler<GetInvoicePdfQuery, InvoicePdfResult>
{
public async ValueTask<InvoicePdfResult> Handle(GetInvoicePdfQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
// BillingDbContext is not tenant-filtered: root may download ANY tenant's invoice PDF; a tenant
// caller is pinned to its own, so a cross-tenant id resolves to 404 and never leaks a PDF.
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.");
var dto = invoice.ToDto();
var content = renderer.Render(dto);
return new InvoicePdfResult(content, $"{dto.InvoiceNumber}.pdf");
}
}
View on GitHub (pinned to 3f2959e683)