{"record":{"id":"2453a18a0d2df9e7","repo":"FlowiseAI/Flowise","slug":"user-is-not-a-member-of-this-channel","errorCode":null,"errorMessage":"User is not a member of this channel","messagePattern":"User is not a member of this channel","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/components/nodes/tools/MicrosoftTeams/core.ts","lineNumber":526,"sourceCode":"        super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })\n    }\n\n    protected async _call(arg: any): Promise<string> {\n        const params = { ...arg, ...this.defaultParams }\n        const { teamId, channelId, userId } = params\n\n        if (!teamId || !channelId || !userId) {\n            throw new Error('Team ID, Channel ID, and User ID are all required')\n        }\n\n        try {\n            // First get the membership ID\n            const membersEndpoint = `/teams/${teamId}/channels/${channelId}/members`\n            const membersResult = await this.makeTeamsRequest(membersEndpoint)\n\n            const member = membersResult.value?.find((m: any) => m.userId === userId)\n            if (!member) {\n                throw new Error('User is not a member of this channel')\n            }\n\n            const endpoint = `/teams/${teamId}/channels/${channelId}/members/${member.id}`\n            await this.makeTeamsRequest(endpoint, 'DELETE')\n\n            return this.formatResponse(\n                {\n                    success: true,\n                    message: 'Member removed from channel successfully'\n                },\n                params\n            )\n        } catch (error) {\n            return this.formatResponse(`Error removing channel member: ${error}`, params)\n        }\n    }\n}\n","sourceCodeStart":508,"sourceCodeEnd":544,"githubUrl":"https://github.com/FlowiseAI/Flowise/blob/abe4a8601a058047b350c260676826e21dd14101/packages/components/nodes/tools/MicrosoftTeams/core.ts#L508-L544","documentation":"Thrown INSIDE the try block of RemoveChannelMemberTool._call (core.ts:525) when the members list returned by Graph contains no entry whose userId matches the requested userId. Because it is inside try, the catch at core.ts:539 swallows it into a formatResponse error string — the tool returns a string containing 'Error removing channel member: Error: User is not a member of this channel', it does NOT reject. The lookup uses strict equality on m.userId, so a UPN/email passed as userId will never match Graph's GUID-shaped userId field even if the user is a member.","triggerScenarios":"userId provided is not a current channel member; userId is a UPN/email while Graph returns GUIDs; membership was removed between the list and the delete (TOCTOU); defaultParams.userId stale from a previous context; the channel is a standard channel and the user is a member only transitively via team membership — Graph still lists them, but the userId field may differ.","commonSituations":"Agent reusing a userId from a different channel's member list; UI showing a user who left the channel; test fixtures using fake GUIDs; tenant sync lag.","solutions":["Verify the userId is an Azure AD object ID (GUID format) and matches a value previously returned by list_channel_members.","If you only have UPN/email, resolve it via /users first and use the returned id.","Parse the tool's return string for 'User is not a member' and treat as a no-op success rather than a hard failure.","Refactor to move the lookup outside try or to a typed Result so callers can distinguish 'not a member' from a real Graph error.","Consider idempotent semantics: removing a non-member should be 204, not an error."],"exampleFix":"// before — strict GUID equality, throws and gets swallowed\nconst member = membersResult.value?.find((m: any) => m.userId === userId)\nif (!member) {\n    throw new Error('User is not a member of this channel')\n}\n\n// after — return a structured no-op result, also fall back to email/UPN match\nconst member = membersResult.value?.find(\n    (m: any) => m.userId === userId || m.email === userId || m.user?.id === userId\n)\nif (!member) {\n    return this.formatResponse(\n        { success: true, message: 'User was not a member; nothing to remove', idempotent: true },\n        params\n    )\n}","handlingStrategy":"validation","validationCode":"// Resolve the membership ID BEFORE calling remove, so the tool call is a no-op-safe\nasync function resolveMembershipId(tool, teamId: string, channelId: string, userId: string): Promise<string | null> {\n  const raw = await tool.listChannelMembers({ teamId, channelId })\n  const members = JSON.parse(raw.split(TOOL_ARGS_PREFIX)[0]).members as Array<{ userId?: string; id: string; email?: string }>\n  const m = members.find((x) => x.userId === userId || x.email === userId)\n  return m?.id ?? null\n}\n\nif (await resolveMembershipId(tools, teamId, channelId, userId) === null) {\n  return { success: true, message: 'User was not a member; nothing to remove', idempotent: true }\n}","typeGuard":"function isChannelMemberShape(m: unknown): m is { userId: string; id: string; email?: string } {\n  return typeof m === 'object' && m !== null &&\n    typeof (m as any).id === 'string' &&\n    typeof (m as any).userId === 'string'\n}","tryCatchPattern":"// This error is swallowed into a response string — parse it\nconst raw = await removeChannelMemberTool.invoke(input)\nconst body = JSON.parse(raw.split(TOOL_ARGS_PREFIX)[0])\nif (/User is not a member/.test(JSON.stringify(body))) {\n  // idempotent success — user already removed\n  return { success: true, idempotent: true }\n}\nif (!body.success) throw new Error(body.message ?? 'remove failed')","preventionTips":["Always resolve membership via list_channel_members first and pass the verified GUID.","Treat 'not a member' as idempotent success, not an error.","Remember this throw is swallowed by the tool's catch — it appears inside the returned string, not as a rejection.","Guard against TOCTOU by treating a missing member as a successful no-op."],"tags":["validation","microsoft-teams","members","lookup-miss","user-id","swallowed-error","toctou"],"backgroundTag":null,"analyzedSha":"abe4a8601a058047b350c260676826e21dd14101","analyzedAt":"2026-08-12T16:04:40.823Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}