feat: reject truncated UUID identifiers locally instead of returning an opaque 404#95
Merged
Merged
Conversation
…an opaque 404
Every method taking a post_id / comment_id / parent_id / user_id / webhook_id /
notification_id now rejects a value that is visibly a FRAGMENT of a UUID —
hex-and-hyphens, 8+ chars, not a whole id — with a ValueError naming the
parameter, both lengths, and the fix.
The failure this catches: an id printed truncated for display (post["id"][:8]
into a log, a table, a code review) and then passed back in as though it were
the whole value. Today that builds a perfectly well-formed request, and the
server answers with a bare 404 Not Found — which reads as "the post was deleted"
when the real cause is "you passed eight characters". Those are debugged very
differently, and the second one is invisible. (Found the hard way: I did exactly
this to the Colony API and only the server's 404 caught it.)
NOT a breaking change. The check is deliberately narrow — opaque placeholders
("p1", "c1", "abc", "post-1") pass through to the server untouched, so mocked
test suites keep working. The 8-char floor is the canonical display truncation
(id[:8], the git short-hash convention); below it a short hex-ish string is far
more plausibly a fixture than a fragment of a real id. Full suite: 965 -> 986
passed, zero existing tests touched.
A SHAPE check, not an EXISTENCE check, and deliberately not sold as one: a
well-formed UUID that refers to nothing still reaches the server and still 404s.
That is the server's job and it is the only party that can do it. A local check
can tell you an id is malformed; it can never tell you an id is real. There is a
test asserting precisely that, so the guard cannot quietly get oversold later.
Applied symmetrically to ColonyClient and AsyncColonyClient (57 methods each);
async_client imports the helper from client, as it already does for _UUID_RE.
No version bump — CHANGELOG entry lands under Unreleased for the next release.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
codecov/patch failed: the UUID guard added a line to every id-taking method, and five async methods — crosspost, pin_post, close_post, reopen_post, set_post_language — had NO tests at all. Their sync twins are covered; the async ones never were. The guard line simply made an existing hole visible. Adds AsyncColonyClient coverage for all five: correct verb and path, crosspost's optional title omitted when unset, set_post_language's code in the query string, and a parametrized case asserting the guard fires BEFORE any request is built — the mock transport raises if it is ever called, so a truncated id provably never leaves the process. Uncovered added lines: 5 → 0. Suite 986 → 997 passed.
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.
The bug
An id gets printed truncated for display —
post["id"][:8]into a log, a table, a code review — and then passed back in as though it were the whole value.Today that builds a perfectly well-formed request, and the server answers with a bare
404 Not Found. Which reads as "the post was deleted" when the real cause is "you passed eight characters". Those are debugged very differently, and the second one is invisible.I found this the hard way: I listed some Colony comments with
print(c["id"][:8]), then used those 8-character strings asparent_id. Only the server's 404 caught it.The fix
Every method taking
post_id/comment_id/parent_id/user_id/webhook_id/notification_idnow rejects a value that is visibly a fragment of a UUID — hex-and-hyphens, 8+ characters, not a whole id:Names the parameter, both lengths, and what to do instead.
Not a breaking change
The check is deliberately narrow. Opaque placeholders —
"p1","c1","abc","post-1"— pass through to the server untouched, exactly as before, so mocked test suites keep working (ours and downstream's).The 8-character floor is what makes that work. It's the canonical display truncation (
id[:8], the git short-hash convention), so it's what real truncation looks like. Below it, a short hex-ish string like"c1"or"abc"is far more plausibly a fixture than a fragment of a real id.Full suite: 965 → 986 passed. Zero existing tests modified. Additive only.
A shape check, not an existence check
Stated plainly because it matters, and asserted in a test so it can't quietly get oversold later:
A well-formed UUID that refers to nothing still reaches the server and still returns 404. That is the server's job, and the server is the only party that can do it. A local check can tell you an id is malformed. It can never tell you an id is real.
That test exists to keep the guard honest about what it does not do.
Scope
ColonyClientandAsyncColonyClient, 57 methods each, applied symmetrically.async_clientimports the helper fromclient, as it already does for_UUID_RE.id) raise aValueErrorthat points at the'id'field.Unreleased— ships whenever the next release goes out.tests/test_uuid_validation.py. ruff + mypy clean.Possible follow-up (not in this PR)
The narrow check catches malformed ids. It cannot catch a syntactically perfect but wrong id. The fix for that class is provenance, not shape: opaque
PostId/CommentIdtypes constructible only from an API response, so a fabricated id becomes a type error at construction rather than a runtime 404. Happy to open a design issue if there's appetite.