{"record":{"id":"1afcbf5412b00f9f","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-getinvoicepdfqueryhandler","errorCode":null,"errorMessage":"Tenant context is required.","messagePattern":"Tenant context is required\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetInvoicePdf/GetInvoicePdfQueryHandler.cs","lineNumber":24,"sourceCode":"using Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Billing.Features.v1.Invoices.GetInvoicePdf;\n\npublic sealed class GetInvoicePdfQueryHandler(\n    BillingDbContext dbContext,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor,\n    IInvoicePdfRenderer renderer)\n    : IQueryHandler<GetInvoicePdfQuery, InvoicePdfResult>\n{\n    public async ValueTask<InvoicePdfResult> Handle(GetInvoicePdfQuery query, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\n\n        // BillingDbContext is not tenant-filtered: root may download ANY tenant's invoice PDF; a tenant\n        // caller is pinned to its own, so a cross-tenant id resolves to 404 and never leaks a PDF.\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;\n\n        var invoice = await dbContext.Invoices.AsNoTracking()\n            .Include(i => i.LineItems)\n            .FirstOrDefaultAsync(\n                i => i.Id == query.InvoiceId && (isRoot || i.TenantId == callerTenantId),\n                cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Invoice {query.InvoiceId} not found.\");\n\n        var dto = invoice.ToDto();\n        var content = renderer.Render(dto);\n        return new InvoicePdfResult(content, $\"{dto.InvoiceNumber}.pdf\");\n    }\n}\n","sourceCodeStart":6,"sourceCodeEnd":40,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetInvoicePdf/GetInvoicePdfQueryHandler.cs#L6-L40","documentation":"GetInvoicePdfQueryHandler mirrors GetInvoiceById but renders a PDF. Because BillingDbContext is not tenant-filtered, it first requires a tenant context (UnauthorizedException if TenantInfo is null) and then pins non-root callers to their own tenant when locating the invoice. Without a resolvable tenant the handler refuses to run rather than risking leaking a PDF.","triggerScenarios":"GET /invoices/{id}/pdf (v1) with no tenant resolved: missing __tenant__ token/header, unregistered host, request outside Finbuckle middleware, or non-HTTP invocation (job/test) without a tenant scope.","commonSituations":"Opening the PDF URL directly in a new browser tab where the app's tenant-setting interceptor/headers are not applied; curl/Postman tests that omit the tenant header; misconfigured tenant host mapping after a domain change.","solutions":["Send the tenant identifier with the request (X-Tenant-Id header or __tenant__ query parameter) so Finbuckle resolves TenantInfo.","When opening the PDF in a new tab, build the URL to include the tenant token or use an authenticated fetch/blob download instead of a raw link.","Confirm multitenancy middleware and host/header strategies are configured for the deployment hostname.","For background PDF generation, execute inside an explicit tenant scope."],"exampleFix":"// before: raw link loses tenant headers\n<a href={`/invoices/${id}/pdf`} target=\"_blank\" />\n\n// after: fetch with apiFetch (injects tenant header), then download\nconst blob = await apiFetch(`/invoices/${id}/pdf`);\ndownloadBlob(blob, `${invoiceNumber}.pdf`);","handlingStrategy":"validation","validationCode":"if (!tenantId) throw new Error('Set tenant context (X-Tenant-Id/__tenant__) before requesting invoice PDFs');\nconst inv = await apiFetch(`/invoices/${id}`); // 404 => no PDF either\nif (!inv) return;","typeGuard":"function hasTenant(t) { return typeof t === 'string' && t.length > 0; }","tryCatchPattern":"try { return await apiFetch(`/invoices/${id}/pdf`); }\ncatch (e) { if (isUnauthorized(e)) { promptTenantSelection(); } throw e; }","preventionTips":["Download PDFs via authenticated apiFetch (blob) instead of raw <a href> links so tenant headers are applied.","Ensure the tenant token is present in any URL opened directly in a new tab.","Confirm the host strategy matches the public hostname after DNS/proxy changes."],"tags":["multitenancy","authorization","billing","pdf"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}