Skip to content

fix(chess_com): resolve mixed-case handles instead of erroring on the 301 - #558

Merged
brunolm merged 1 commit into
kaifcodec:mainfrom
brunolm:fix/chess-com-mixed-case-handles
Aug 9, 2026
Merged

fix(chess_com): resolve mixed-case handles instead of erroring on the 301#558
brunolm merged 1 commit into
kaifcodec:mainfrom
brunolm:fix/chess-com-mixed-case-handles

Conversation

@brunolm

@brunolm brunolm commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

tl;dr

  • Every mixed-case chess.com handle returned an error instead of a verdict. api.chess.com 301s any non-lowercase handle to its lowercase form and generic_validate does not follow redirects, so MagnusCarlsen produced Unexpected status code: 301 while magnuscarlsen resolved fine.
  • Fixed by lowercasing the handle, not by following redirects — one request instead of two, and an unexpected 3xx stays an error rather than being silently followed.
  • music/audiojungle.py has the identical bug and is deliberately not fixed here. It reports real accounts as ERROR: HTTP 301 at their own display casing. Separate module, separate PR.
  • show_url keeps the caller's casing, because www.chess.com/member/ is case-insensitive and the API's own url field is the mixed-case display form.
  • Two pre-existing quirks left alone — invalid handles return available, and a 0-follower account drops the followers field entirely. Both are distinct from the redirect.

Every mixed-case chess.com handle returned an error instead of a verdict

The API canonicalises casing with a redirect, and it does so regardless of whether the account exists:

api.chess.com/pub/player/magnuscarlsen            -> 200
api.chess.com/pub/player/MagnusCarlsen            -> 301 -> .../magnuscarlsen
api.chess.com/pub/player/zzznotarealchessuser99xq -> 404
api.chess.com/pub/player/ZzzNotARealChessUser99xq -> 301 -> .../zzznotarealchessuser99xq

generic_validate (httpx) does not follow redirects, so the 301 fell through to the module's Unexpected status code branch. Handles are case-insensitive on chess.com, so the casing a user types is arbitrary — any capital letter made the module useless for that handle.

Fixed by lowercasing the handle, not by following redirects

url = f"https://api.chess.com/pub/player/{user.lower()}"

Both candidate fixes were checked against the live API. follow_redirects=True also works, but lowercasing was chosen because:

Lowercase the handle follow_redirects=True
Requests per lookup 1 2
Meaning of an unexpected 3xx still Result.error silently followed, final status becomes the verdict
Depends on redirect target no yes

The redirect-target assumption was swept across 85 handles — 70 sampled at random from the public team-usa club roster (ordinary free accounts, not public figures) plus 15 invented ones, of which 71 resolved and 14 did not. For each, the lowercase form was requested directly and a randomly-capitalised variant was requested alongside it:

Check Result
Lowercase handles that redirected 0 / 85
Mixed-case 301s pointing anywhere but str.lower(input) 0 / 85
Verdict disagreement between lower() and follow_redirects=True 0 / 85

Lowercasing bakes in an assumption the API could change

If chess.com ever adds a redirect that is not a lowercasing — a renamed-account alias, say — this module will 404 where following redirects would have resolved it. That trade was taken on purpose: a wrong available is the worse failure, and an alias redirect would surface here as Result.error on an unexpected 3xx rather than as a silent miss.

The sweep above found no such alias, but it samples behaviour rather than proving it; chess.com publishes no guarantee either way.

There is also no handle whose canonical API form is mixed case. The JSON username field is always lowercase; the mixed-case form lives only in the url field as a display string:

{"username": "magnuscarlsen", "url": "https://www.chess.com/member/MagnusCarlsen"}

music/audiojungle.py has the identical bug and is deliberately not fixed here

122 user_scan modules call generic_validate; 85 of them have no 3xx branch. All 85 were probed at two casings, and one is broken the same way:

audiojungle.net/user/Soundroll -> 301 -> /user/soundroll

Soundroll   ERROR  HTTP 301
soundroll   TAKEN  {'joined': 'February 2009', 'followers': 5780, 'following': 3}
AlexGrohl   ERROR  HTTP 301
alexgrohl   TAKEN  {'joined': 'January 2016', 'followers': 812, 'following': 102}

These are real Envato author storefronts whose display casing is mixed, so the spelling a user would naturally type is the one that fails. It is left out of this PR because it is a different module with a different site to re-verify, and folding it in would make both harder to review.

The other casing differences the sweep turned up are not this bug and need no change: codewars is genuinely case-sensitive (404 lowercase, 200 mixed, no redirect), dockerhub is a lowercase-only namespace, and minds / warpcast differ in the body and query layers with no 3xx involved.

show_url keeps the caller's casing

www.chess.com/member/ resolves at any casing — verified on public figures, on ordinary free accounts, and on a nonexistent handle:

www.chess.com/member/MagnusCarlsen            -> 200
www.chess.com/member/magnuscarlsen            -> 200
www.chess.com/member/ZzzNotARealChessUser99xq -> 404

So the human-facing link was left as the caller typed it.

Two pre-existing quirks left alone

Invalid handles return available. The three length/charset guards fire before any request:

if not (3 <= len(user) <= 25):
    return Result.available("Username must be between 3 and 25 characters")

Per this repo's own rule — "anything that is neither a confirmed found nor a confirmed not-found returns Result.error" — an unregistrable name is not a free name, so error looks more correct.

A 0-follower account drops the field. if followers := data.get("followers") treats 0 as missing, so extra omits followers entirely rather than reporting 0. Surfaced while testing free accounts.

Neither is changed here: both alter output for inputs unrelated to the redirect, and mixing them into a redirect fix would make all three harder to review. Flagged rather than fixed. Also unchanged: the API returns player_id, country and joined, which the module does not surface.

Testing

Live against the real API, every case run against main and against this branch. Verdicts before and after:

Handle Before After
magnuscarlsen Found Found
MagnusCarlsen error: Unexpected status code: 301 Found
hikarunakamura Found Found
HikaruNakamura error: Unexpected status code: 301 Found
gothamchess Found Found
GothamChess error: Unexpected status code: 301 Found
10 ordinary free accounts, lowercase Found Found
the same 10, mixed case error: Unexpected status code: 301 Found
zzznotarealchessuser99xq Not Found Not Found
ZzzNotARealChessUser99xq error: Unexpected status code: 301 Not Found
ab, a×26, bad handle!, -lead, trail- available + reason available + reason

The nonexistent handle is the one that matters most: ZzzNotARealChessUser99xq now returns Not Found, not Found and not error. Every lowercase verdict is byte-identical before and after.

Handles of ordinary members are redacted; they were sampled from the public team-usa club roster.

Free accounts now cover the falsy metadata branches, which the earlier round of testing missed by using only titled public figures:

Account shape Fields correctly absent from extra
free, 0 followers title, league, location, followers
free, empty profile name, title, location, avatar (no media)
free, inactive league

Non-ASCII input is unreachable, not merely untested. Scanning every codepoint in 0x00x10FFFF, exactly one character outside [a-zA-Z0-9_-] slips past the charset guard — \n, because Python's $ matches before a trailing newline — and the next guard (user[-1].isalnum()) rejects it. Brute-forcing all 246,016 guard-passing 3-character handles confirms .lower() is total, length-preserving and idempotent over the whole alphabet, so the İ length-change class of hazard cannot reach the call.

Metadata is identical between casings, and the values belong to the requested account:

{"name": "Magnus Carlsen", "username": "magnuscarlsen", "title": "GM",
 "status": "premium", "league": "Champion", "location": "Norway", "followers": "309323"}

Exercised through the CLI as well as by direct calls:

python -m user_scanner -u MagnusCarlsen -m chess_com -f json -o output/x.json
-> "status": "Found", "url": "https://www.chess.com/member/MagnusCarlsen"

Not tested

  • Only free and titled accounts were exercised. staff and closed/closed:fair_play_violations account states were not reached, so their status values are unverified.

… 301

The API 301-redirects any non-lowercase handle to its lowercase canonical
form. generic_validate does not follow redirects, so every mixed-case handle
returned "Unexpected status code: 301" instead of a verdict.

Request the lowercased handle directly. This keeps the lookup at one request
and leaves an unexpected 3xx meaningful as an error rather than something the
transport silently follows. show_url keeps the caller's casing, which
www.chess.com/member/ resolves case-insensitively.
@brunolm
brunolm marked this pull request as ready for review August 9, 2026 16:29
@brunolm brunolm added the bug fix Fixed a bug in existing files label Aug 9, 2026
@brunolm
brunolm merged commit b0c35c9 into kaifcodec:main Aug 9, 2026
2 checks passed
@brunolm
brunolm deleted the fix/chess-com-mixed-case-handles branch August 9, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug fix Fixed a bug in existing files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants