fix(chess_com): resolve mixed-case handles instead of erroring on the 301 - #558
Merged
Merged
Conversation
… 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
marked this pull request as ready for review
August 9, 2026 16:29
kristoisberg
approved these changes
Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tl;dr
api.chess.com301s any non-lowercase handle to its lowercase form andgeneric_validatedoes not follow redirects, soMagnusCarlsenproducedUnexpected status code: 301whilemagnuscarlsenresolved fine.music/audiojungle.pyhas the identical bug and is deliberately not fixed here. It reports real accounts asERROR: HTTP 301at their own display casing. Separate module, separate PR.show_urlkeeps the caller's casing, becausewww.chess.com/member/is case-insensitive and the API's ownurlfield is the mixed-case display form.available, and a 0-follower account drops thefollowersfield 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:
generic_validate(httpx) does not follow redirects, so the 301 fell through to the module'sUnexpected status codebranch. 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
Both candidate fixes were checked against the live API.
follow_redirects=Truealso works, but lowercasing was chosen because:follow_redirects=TrueResult.errorThe redirect-target assumption was swept across 85 handles — 70 sampled at random from the public
team-usaclub 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:str.lower(input)lower()andfollow_redirects=TrueLowercasing 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
availableis the worse failure, and an alias redirect would surface here asResult.erroron 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
usernamefield is always lowercase; the mixed-case form lives only in theurlfield as a display string:{"username": "magnuscarlsen", "url": "https://www.chess.com/member/MagnusCarlsen"}music/audiojungle.pyhas the identical bug and is deliberately not fixed here122
user_scanmodules callgeneric_validate; 85 of them have no 3xx branch. All 85 were probed at two casings, and one is broken the same way: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:
codewarsis genuinely case-sensitive (404 lowercase, 200 mixed, no redirect),dockerhubis a lowercase-only namespace, andminds/warpcastdiffer in the body and query layers with no 3xx involved.show_urlkeeps the caller's casingwww.chess.com/member/resolves at any casing — verified on public figures, on ordinary free accounts, and on a nonexistent handle: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: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, soerrorlooks more correct.A 0-follower account drops the field.
if followers := data.get("followers")treats0as missing, soextraomitsfollowersentirely rather than reporting0. 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,countryandjoined, which the module does not surface.Testing
Live against the real API, every case run against
mainand against this branch. Verdicts before and after:magnuscarlsenMagnusCarlsenhikarunakamuraHikaruNakamuragothamchessGothamChesszzznotarealchessuser99xqZzzNotARealChessUser99xqab,a×26,bad handle!,-lead,trail-The nonexistent handle is the one that matters most:
ZzzNotARealChessUser99xqnow 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-usaclub roster.Free accounts now cover the falsy metadata branches, which the earlier round of testing missed by using only titled public figures:
extratitle,league,location,followersname,title,location,avatar(nomedia)leagueNon-ASCII input is unreachable, not merely untested. Scanning every codepoint in
0x0–0x10FFFF, 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İ→i̇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:
Not tested
staffandclosed/closed:fair_play_violationsaccount states were not reached, so theirstatusvalues are unverified.