fix(api): scouting endpoints return 200 on auth failure, breaking Google Sheets clients #213

Closed
opened 2026-04-11 01:14:05 +00:00 by cal · 1 comment
Owner

Problem

The /api/v2/battingcardratings/scouting and /api/v2/pitchingcardratings/scouting endpoints return HTTP 200 even when the auth/guide gate fails. The failure body is a plain text string:

"Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): https://ko-fi.com/manticorum/shop"

Google Sheets clients consuming these endpoints as CSV treat the 200 as success and try to parse the error string as CSV data, producing confusing downstream errors ("team does not have the guide") even when has_guide=true.

Repro (real incident)

Team 100 (Gauntlet-EXW, "Exploding Whales") had has_guide=true but got the error. Root cause was a stale ts hash in the sheet after an sname rename — team_hash() is derived from sname[-1] and sname[-2], so any rename invalidates cached hashes.

  • API call: GET /api/v2/battingcardratings/scouting?team_id=100&ts=s67402603023w11135396893 → 200 (with error string body)
  • Expected hash: s67402603023e11135396893
  • Sent hash: s67402603023w11135396893

Fix

In database/app/routers_v2/battingcardratings.py and database/app/routers_v2/pitchingcardratings.py, the get_card_scouting handlers should raise HTTPException(status_code=403, detail=...) on gate failure instead of returning the message as a 200 response body.

Current (battingcardratings.py:329)

@router.get("/scouting")
async def get_card_scouting(team_id: int, ts: str):
    this_team = Team.get_or_none(Team.id == team_id)
    if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
        logging.warning(f"Team_id {team_id} attempted to pull ratings")
        return (
            "Your team does not have the ratings guide enabled. ..."
        )

Desired

@router.get("/scouting")
async def get_card_scouting(team_id: int, ts: str):
    this_team = Team.get_or_none(Team.id == team_id)
    if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
        logging.warning(f"Team_id {team_id} attempted to pull ratings")
        raise HTTPException(
            status_code=403,
            detail="Your team does not have the ratings guide enabled. ...",
        )

Apply the same change to pitchingcardratings.py.

Acceptance

  • Both /scouting endpoints return 403 (not 200) on gate failure
  • Success path still returns the CSV FileResponse unchanged
  • Sheets clients now see a clear HTTP error instead of parsing error text as CSV
## Problem The `/api/v2/battingcardratings/scouting` and `/api/v2/pitchingcardratings/scouting` endpoints return **HTTP 200** even when the auth/guide gate fails. The failure body is a plain text string: > "Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): https://ko-fi.com/manticorum/shop" Google Sheets clients consuming these endpoints as CSV treat the 200 as success and try to parse the error string as CSV data, producing confusing downstream errors ("team does not have the guide") even when `has_guide=true`. ## Repro (real incident) Team 100 (Gauntlet-EXW, "Exploding Whales") had `has_guide=true` but got the error. Root cause was a stale `ts` hash in the sheet after an `sname` rename — `team_hash()` is derived from `sname[-1]` and `sname[-2]`, so any rename invalidates cached hashes. - API call: `GET /api/v2/battingcardratings/scouting?team_id=100&ts=s67402603023w11135396893` → 200 (with error string body) - Expected hash: `s67402603023e11135396893` - Sent hash: `s67402603023w11135396893` ## Fix In `database/app/routers_v2/battingcardratings.py` and `database/app/routers_v2/pitchingcardratings.py`, the `get_card_scouting` handlers should `raise HTTPException(status_code=403, detail=...)` on gate failure instead of returning the message as a 200 response body. ### Current (battingcardratings.py:329) ```python @router.get("/scouting") async def get_card_scouting(team_id: int, ts: str): this_team = Team.get_or_none(Team.id == team_id) if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: logging.warning(f"Team_id {team_id} attempted to pull ratings") return ( "Your team does not have the ratings guide enabled. ..." ) ``` ### Desired ```python @router.get("/scouting") async def get_card_scouting(team_id: int, ts: str): this_team = Team.get_or_none(Team.id == team_id) if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: logging.warning(f"Team_id {team_id} attempted to pull ratings") raise HTTPException( status_code=403, detail="Your team does not have the ratings guide enabled. ...", ) ``` Apply the same change to `pitchingcardratings.py`. ## Acceptance - [ ] Both `/scouting` endpoints return 403 (not 200) on gate failure - [ ] Success path still returns the CSV FileResponse unchanged - [ ] Sheets clients now see a clear HTTP error instead of parsing error text as CSV
cal added the
bug
label 2026-04-11 01:14:05 +00:00
Claude added the
ai-working
label 2026-04-11 01:31:09 +00:00
Claude removed the
ai-working
label 2026-04-11 01:32:07 +00:00
Collaborator

PR #214 fixes this. Both /scouting endpoints now raise HTTPException(status_code=403, ...) instead of return-ing the error string as a 200 body. HTTPException was already imported — minimal two-line change per file.

PR #214 fixes this. Both `/scouting` endpoints now `raise HTTPException(status_code=403, ...)` instead of `return`-ing the error string as a 200 body. `HTTPException` was already imported — minimal two-line change per file.
Claude added the
ai-pr-opened
label 2026-04-11 01:32:14 +00:00
cal closed this issue 2026-04-11 01:49:13 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: cal/paper-dynasty-database#213
No description provided.