{"record":{"id":"3b6d5289267e8ca6","repo":"fullstackhero/dotnet-starter-kit","slug":"invalid-tenant-requestuploadurlcommandhandler","errorCode":null,"errorMessage":"invalid tenant","messagePattern":"invalid tenant","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Features/v1/RequestUploadUrl/RequestUploadUrlCommandHandler.cs","lineNumber":31,"sourceCode":"using Mediator;\nusing Microsoft.Extensions.Options;\n\nnamespace FSH.Modules.Files.Features.v1.RequestUploadUrl;\n\npublic sealed class RequestUploadUrlCommandHandler(\n    FilesDbContext db,\n    IStorageService storage,\n    FileAccessPolicyRegistry policies,\n    IQuotaService quotas,\n    ICurrentUser currentUser,\n    IOptions<FilesOptions> options)\n    : ICommandHandler<RequestUploadUrlCommand, PresignedUploadResponse>\n{\n    public async ValueTask<PresignedUploadResponse> Handle(RequestUploadUrlCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n\n        var tenantId = currentUser.GetTenant() ?? throw new UnauthorizedException(\"invalid tenant\");\n        var userId = currentUser.GetUserId();\n        if (userId == Guid.Empty)\n        {\n            throw new UnauthorizedException(\"no current user\");\n        }\n\n        // Category lookup + extension/size validation.\n        if (!options.Value.Categories.TryGetValue(cmd.Category, out var category))\n        {\n            throw new CustomException($\"Unknown category '{cmd.Category}'.\", (IEnumerable<string>?)null, HttpStatusCode.BadRequest);\n        }\n\n        var extension = Path.GetExtension(cmd.FileName);\n        if (string.IsNullOrWhiteSpace(extension) ||\n            !category.AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))\n        {\n            throw new CustomException(\n                $\"Extension '{extension}' not allowed for category '{cmd.Category}'.\",","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/RequestUploadUrl/RequestUploadUrlCommandHandler.cs#L13-L49","documentation":"UnauthorizedException('invalid tenant') is thrown by RequestUploadUrlCommandHandler.Handle when the current user's JWT has no tenant claim (currentUser.GetTenant() returns null). The Files module is tenant-scoped, so a presigned upload URL can only be issued once the request is bound to a tenant via Finbuckle multitenancy.","triggerScenarios":"Calling POST /files/v1/request-upload-url (RequestUploadUrlCommand) with an access token that lacks the tenant identifier claim — e.g. a token issued for a non-tenant context, a token whose 'tenant' claim was dropped, or an unauthenticated/misrouted request where the tenant could not be resolved from the header/host route.","commonSituations":"Tokens minted outside the normal login flow (service tokens, stale tokens from before tenancy was added); clients forgetting the tenant header/route the API host expects; hosting/mapping misconfiguration so Finbuckle cannot resolve the tenant from the request; testing with raw tokens copied from another environment.","solutions":["Log in again through the normal auth flow so the token includes the tenant claim, and retry the upload request.","Send the request to the tenant-resolvable route/host or add the tenant identifier header the API's Finbuddle multitenancy strategy expects.","Verify the token contents (decode the JWT) and confirm a tenant claim exists; if missing, fix token issuance in the identity layer.","If the user genuinely has no tenant, assign the user to a tenant before performing file operations."],"exampleFix":"// before\nclient.PostAsJsonAsync(\"/files/v1/request-upload-url\", cmd); // token had no tenant claim\n// after\nrequest.Headers.Add(\"tenant\", tenantId); // or use the tenant-routed host, with a freshly issued token","handlingStrategy":"validation","validationCode":"const tenantId = getTenantIdFromToken(token);\nif (!tenantId) throw new Error(\"Token has no tenant claim — re-authenticate or send the tenant header\");","typeGuard":"function hasTenant(claims) { return typeof claims.tenant === \"string\" && claims.tenant.length > 0; }","tryCatchPattern":"try { await requestUploadUrl(cmd); } catch (e) { if (e.status === 401 && e.message === \"invalid tenant\") { await reauthenticateWithTenantContext(); } else throw e; }","preventionTips":["Always call the API through the tenant-routed host or include the tenant identifier header.","Re-authenticate after any tenancy config change instead of reusing old tokens.","Decode tokens in dev tooling to verify the tenant claim before debugging deeper."],"tags":["multitenancy","authorization","files","jwt"],"backgroundTag":"missing-credentials","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"}