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

  1. Verify the invoice id exists in the Invoices table and, for non-root callers, that its TenantId matches the caller.
  2. Re-fetch the invoice list (GET /invoices) and derive the PDF link from a current DTO instead of a stored URL.
  3. For cross-tenant PDF access, authenticate as root; otherwise the TenantId filter will yield a 404.
  4. 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

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


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)