Skip to content

People: "Mutual connections" is silently dead — /mutual returns counts, not a user array #160

Description

@Adron

The feature renders nothing, and fails silently

the-gaps.md records "Mutual connections on a profile (People) — chips that open that user" as shipped in session 6. It has never displayed anything.

Found by the contract-test suite (#126 / PR #159) and confirmed independently.

Why

GET /api/follow/{userId}/mutual returns counts, not users:

GET /api/follow/c65092fa-…/mutual  -> {"mutualFollowers":0,"mutualFollowing":0}
GET /api/follow/15e3d575-…/mutual  -> {"mutualFollowers":1,"mutualFollowing":1}
GET /api/follow/15e3d575-…/mutual?otherUserId=c65092fa-…
                                   -> {"mutualFollowers":1,"mutualFollowing":1}

But the client asks for an array under a mutual key:

public Task<List<FollowUser>> GetMutualAsync(string userId, …)
    => GetUserArrayAsync($"api/follow/{userId}/mutual", "mutual", ct);

…and GetUserArrayAsync is deliberately tolerant:

return json.TryGetProperty(property, out var arr) && arr.ValueKind == JsonValueKind.Array
    ? arr.Deserialize<List<FollowUser>>(JsonOptions) ?? new()
    : new();     // <-- silently empty

So it returns an empty list, HasMutuals is false, the chips section collapses, and nothing indicates a problem. That tolerance is right for a genuinely optional array — it's exactly what hides this.

The API does not support the built feature

I checked for an alternative. The only mutual endpoint is this one, and both its parameter forms (userId alone, and userId + the spec-documented otherUserId) return the same counts object. There is no endpoint that lists mutual users, so clickable chips cannot be built as designed.

Fix

  • Model the real response ({mutualFollowers, mutualFollowing}) and change GetMutualAsync's return type — its current signature is a promise the API can't keep.
  • Replace the chips with the counts, which is what's actually available.
  • Record on the method that no user-list endpoint exists, so nobody reinstates the chips.

Wider point worth acting on

This is the second bug of the same shape in two days: a tolerant "absent array → empty list" read turning an API mismatch into a silently missing feature. The other was #144 (ListDataRow.ListId), which at least threw. Worth auditing the other GetUserArrayAsync call sites — followers, following, requests — against live payloads; followers and following are confirmed correct (#95), requests is unverified.

Acceptance criteria

  • GetMutualAsync returns the real shape; no caller can expect a user list
  • Profile shows mutual counts
  • Verified against a live payload, not by eye
  • The remaining GetUserArrayAsync key (requests) checked against live data

Activity

  1. added
    bugSomething isn't working
    parityWeb/API feature-parity work
    P1Table stakes
    on Sep 16, 2026
  2. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    Fixed in #162 — work continues in the PR from here.

    GET /api/follow/{userId}/mutual returns {"mutualFollowers":n,"mutualFollowing":n}, and both documented parameter forms return the same counts object — so there is no endpoint that lists mutual users and the chips were never buildable. GetMutualAsync → GetMutualCountsAsync with a typed model; the profile shows counts.

    Audited the sibling GetUserArrayAsync keys against live payloads while here: requests, followers and following are all correct — mutual was the only wrong one. The helper now carries a warning that a wrong key fails as "feature renders nothing" rather than as an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Table stakesarea:peopleArea: peoplebugSomething isn't workingparityWeb/API feature-parity work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions