{"record":{"id":"b61e203388394eb9","repo":"fullstackhero/dotnet-starter-kit","slug":"channel-not-found-channelauthorization","errorCode":null,"errorMessage":"Channel not found.","messagePattern":"Channel not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Internal/ChannelAuthorization.cs","lineNumber":17,"sourceCode":"using FSH.Framework.Core.Exceptions;\nusing FSH.Modules.Chat.Contracts.v1.DTOs;\nusing FSH.Modules.Chat.Domain;\n\nnamespace FSH.Modules.Chat.Features.v1.Internal;\n\n/// <summary>\n/// Small assertion helpers used by channel/message handlers so the rules stay in one place.\n/// Throws framework-aware exceptions so the global handler emits the right HTTP status.\n/// </summary>\ninternal static class ChannelAuthorization\n{\n    public static ChannelMember RequireMember(this ChatChannel channel, string userId)\n    {\n        var member = channel.Members.FirstOrDefault(m => string.Equals(m.UserId, userId, StringComparison.Ordinal));\n        // Use NotFoundException (404) instead of Forbidden so non-members can't probe channel existence.\n        return member ?? throw new NotFoundException(\"Channel not found.\");\n    }\n\n    public static ChannelMember RequireAdmin(this ChatChannel channel, string userId)\n    {\n        var member = channel.RequireMember(userId);\n        if (member.Role != ChannelMemberRole.Admin)\n        {\n            throw new ForbiddenException(\"Channel admin role required.\");\n        }\n        return member;\n    }\n}\n","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Internal/ChannelAuthorization.cs#L1-L30","documentation":"ChannelAuthorization.RequireMember throws NotFoundException(\"Channel not found.\") when the requested userId is not found among channel.Members. The comment explains this is intentional: non-members get 404 instead of 403 Forbidden so they cannot probe whether a channel exists. Thus this error means 'you are not a member', not necessarily 'the channel is missing'.","triggerScenarios":"Any handler calling channel.RequireMember(userId) (e.g. DeleteMessage, EditMessage) where the authenticated user has no ChannelMember row for that channel — never joined, was removed, or membership belongs to another tenant.","commonSituations":"A user attempting to edit/delete messages in a channel they were removed from; a user from another workspace guessing channel/message ids; membership seeded only after an invite event that has not been processed yet.","solutions":["Add the user as a ChannelMember of the channel before performing channel-scoped operations.","Have the caller first verify membership (list user's channels) and skip the operation if absent.","Check whether a membership removal/invite flow ran unexpectedly; re-create the membership row if it was lost.","Do not 'fix' by downgrading to Forbidden — the 404 masking is a deliberate anti-enumeration measure."],"exampleFix":"// before\nchannel.RequireMember(userId); // throws if not a member\n\n// after\nif (channel.Members.Any(m => m.UserId == userId))\n{\n    channel.RequireMember(userId);\n}\nelse\n{\n    channel.Join(userId); // establish membership first\n}","handlingStrategy":"validation","validationCode":"const myChannels = await api.listMyChannels();\nif (!myChannels.some(c => c.id === channelId)) {\n  throw new Error(\"not a member of this channel\"); // skip the operation\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.deleteMessage(messageId);\n} catch (e) {\n  if (e.status === 404) {\n    // 404 here may mean \"not a member\" — treat as access denied, not missing data\n    showNoAccessNotice();\n  } else throw e;\n}","preventionTips":["Check membership before offering channel-scoped actions in the UI.","Refresh membership state after kick/leave events.","Remember 404 is intentional masking for non-members — don't distinguish.","Ensure invite flows complete before the user can act on the channel."],"tags":["chat","authorization","membership","not-found"],"backgroundTag":"entity-not-found","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"}