Stirling-Tools/Stirling-PDF · error · Error

Member has no team to be removed from

Error message

Member has no team to be removed from

What it means

Thrown by the SaaS usersBackend removeMember() when the Member object passed in has a null teamId. The DELETE endpoint requires a concrete teamId in the URL path (/api/v1/team/{teamId}/members/{id}), so without it the request cannot be constructed.

Source

Thrown at frontend/editor/src/saas/portal/usersBackend.ts:245

    const tid = teamId ?? (await resolveTeam())?.teamId;
    if (tid == null) throw new Error("No team to invite to");
    await apiClient.local.json(`/api/v1/team/invite`, {
      method: "POST",
      body: { teamId: tid, email },
    });
    return { successCount: 1, failureCount: 0 };
  },

  async renameTeam(teamId: number, newName: string): Promise<void> {
    await apiClient.local.json(`/api/v1/team/${teamId}/rename`, {
      method: "POST",
      body: { newName },
    });
  },

  async removeMember(member: Member): Promise<void> {
    if (member.teamId == null) {
      throw new Error("Member has no team to be removed from");
    }
    await apiClient.local.json(
      `/api/v1/team/${member.teamId}/members/${member.id}`,
      { method: "DELETE" },
    );
  },

  async cancelInvitation(invitationId: number): Promise<void> {
    await apiClient.local.json(`/api/v1/team/invitations/${invitationId}`, {
      method: "DELETE",
    });
  },
};

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Filter the member list to exclude entries with null teamId before rendering remove buttons
  2. Validate member.teamId in the UI component before enabling the remove action
  3. Ensure the backend member query always includes teamId for every row
  4. Use cancelInvitation() instead of removeMember() for pending invitations

Example fix

// before
async removeMember(member: Member): Promise<void> {
  if (member.teamId == null) {
    throw new Error("Member has no team to be removed from");
  }
  await apiClient.local.json(
    `/api/v1/team/${member.teamId}/members/${member.id}`,
    { method: "DELETE" },
  );
}

// after (in the UI caller)
const membersWithTeam = members.filter((m) => m.teamId != null);
// only render remove buttons for membersWithTeam
Defensive patterns

Strategy: validation

Validate before calling

// Filter member list to only removable members
const removableMembers = members.filter((m) => m.teamId != null);
// Only render remove buttons for removableMembers

Type guard

function isRemovableMember(member: Member): member is Member & { teamId: number } {
  return member.teamId != null;
}

Try / catch

try {
  await removeMember(member);
} catch (e) {
  if (e instanceof Error && e.message.includes('no team')) {
    // Use cancelInvitation for pending members instead
    if (member.invitationId) await cancelInvitation(member.invitationId);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: The member list returned by the backend includes an entry where teamId was not populated — e.g. a pending invitation row, a data inconsistency, or a member from a team that was since deleted. The UI attempts to remove this member.

Common situations: Member list includes invited-but-not-yet-accepted users whose teamId is null; stale member data after a team merge or migration; backend query omits teamId for certain member types.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/c640a1dd1413f1ae. Report an issue: GitHub.