{"record":{"id":"281947e6ff5f1d95","repo":"fullstackhero/dotnet-starter-kit","slug":"invalid-tenant-finalizeuploadcommandhandler","errorCode":null,"errorMessage":"invalid tenant","messagePattern":"invalid tenant","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs","lineNumber":33,"sourceCode":"using FSH.Modules.Files.Services;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Files.Features.v1.FinalizeUpload;\n\npublic sealed class FinalizeUploadCommandHandler(\n    FilesDbContext db,\n    IStorageService storage,\n    IFileScanner scanner,\n    IQuotaService quotas,\n    IOutboxWriter outbox,\n    ICurrentUser currentUser)\n    : ICommandHandler<FinalizeUploadCommand, FileAssetDto>\n{\n    public async ValueTask<FileAssetDto> Handle(FinalizeUploadCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        var tenantId = currentUser.GetTenant() ?? throw new UnauthorizedException(\"invalid tenant\");\n        var userId = currentUser.GetUserId().ToString();\n\n        var asset = await db.FileAssets\n            .FirstOrDefaultAsync(f => f.Id == cmd.FileAssetId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"file not found\");\n\n        if (!string.Equals(asset.CreatedByUserId, userId, StringComparison.Ordinal))\n        {\n            throw new ForbiddenException(\"not your pending file\");\n        }\n        if (asset.Status != FileAssetStatus.PendingUpload)\n        {\n            throw new CustomException(\"file already finalized\", (IEnumerable<string>?)null, HttpStatusCode.Conflict);\n        }\n\n        var head = await storage.HeadObjectAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false)\n            ?? throw new CustomException(\"upload not received\", (IEnumerable<string>?)null, HttpStatusCode.Conflict);","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs#L15-L51","documentation":"FinalizeUploadCommandHandler requires an authenticated tenant context: currentUser.GetTenant() returns null when the request has no tenant claim/binding, and the handler throws UnauthorizedException(\"invalid tenant\"). Finbuckle multitenancy plus the JWT tenant claim feed ICurrentUser; without them the upload cannot be attributed or quota-charged.","triggerScenarios":"Calling the finalize-upload endpoint without a tenant identifier (missing tenant header/route segment, missing 'tenant' claim in the JWT, anonymous or tenant-less service token), or the tenant resolver failing to match the identifier.","commonSituations":"Direct API/script calls that omit the tenant header the UI normally sends; tokens issued by a legacy identity setup without tenant claims; Finbuckle strategy misconfigured (e.g. expected __tenant__ query/header absent); background jobs calling the handler without tenant context.","solutions":["Send the tenant identifier with the request (tenant header/route per the configured Finbuckle strategy).","Ensure the JWT contains the tenant claim when issued, or re-authenticate after fixing token issuance.","Verify the endpoint is not reachable anonymously; require authentication for finalize-upload.","For non-interactive callers, use a properly tenant-scoped service credential rather than calling user endpoints."],"exampleFix":"// before\nfetch('/api/v1/files/finalize', { method: 'POST', ... }); // no tenant\n// after\nfetch('/api/v1/files/finalize', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'tenant': tenantId }, ... });","handlingStrategy":"type-guard","validationCode":"if (string.IsNullOrWhiteSpace(tenantId)) throw new InvalidOperationException(\"Call finalize-upload with a tenant identifier (header/claim) set.\");","typeGuard":"bool HasTenantContext(ICurrentUser user) => user.GetTenant() is not null;","tryCatchPattern":"catch (UnauthorizedException e) when (e.Message == \"invalid tenant\") {\n    redirectToTenantSelection(); // or attach tenant header and retry once\n}","preventionTips":["Always send the tenant header/segment your Finbuckle strategy expects.","Ensure the identity server includes the tenant claim in tokens.","Require authentication on finalize-upload; reject anonymous calls early.","For service-to-service calls, use tenant-scoped credentials, not user endpoints."],"tags":["authentication","multitenancy","files","jwt"],"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"}