Skip to content

chess_com_client: typed fetch result that distinguishes UserNotFound from other 404s #1382

Description

@cursor

Context

Came out of #1360 / #1381. Indexing no longer needs this — IndexWorker correctly treats Optional.empty() from fetchGames as failure and empty months as HTTP 200 {"games":[]}. Filing so the API-shape findings do not evaporate.

What we learned about chess.com 404s

ChessClient.exchange still maps every 404 to Optional.empty(). That collapses two different outcomes:

Case Status x-chesscom-matched Body
Missing player (monthly archive) 404 pubapi_player_games_archive_UserNotFound User "…" not found.
Missing player (archives index) 404 pubapi_player_games_archives_UserNotFound same
Missing player (profile / stats) 404 …_UserNotFound same
Empty month 200 pubapi_player_games_archive {"games":[]}
Listed archive upstream failure (intermittent) 404 (not reproduced in #1360 follow-up; body was internal-error text) An internal error has occurred…

The useful discriminator on the 404s we can hit reliably is x-chesscom-matched ending in _UserNotFound. Secondary tells: LinkDisplayableException.jsonld vs Games.jsonld, and cache-control: max-age=30 vs 5.

Our HttpResponse already exposes headers via getHeaders().

Why callers care

  • IndexWorker — both kinds of games 404 should fail the request; no split required (done in one_d4: treat chess.com archive 404 as indexing failure (#1360) #1381).
  • ChessComGamesTool — today maps every empty Optional to "player not found", which mislabels a listed-archive internal-error 404.
  • Future callers that want “missing player” vs “upstream blew up” without re-deriving header rules.

Proposal

Enrich the client result rather than keeping bare Optional:

  • A small result type (name TBD) that can carry success payload or a structured miss/failure reason.
  • Reason derived from x-chesscom-matched (at least USER_NOT_FOUND vs NOT_FOUND / UPSTREAM_ERROR for non-UserNotFound 404s), optionally retaining status + matched route string for logs.
  • Apply to fetchGames first; fetchPlayer / fetchStats already mean “missing” on empty and can migrate later if useful.

Not in scope until a caller needs the split. Do not change IndexWorker’s “empty Optional = fail” contract without a reason.

Non-goals

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions