Percent-encode ids and query values when building URLs - #36
Draft
samhashemi wants to merge 1 commit into
Draft
Conversation
Ids were interpolated straight into the URL path with str.format, and the five query-string call sites built their query with f-strings. Neither encodes, so a value that is not already URL-safe changes the request rather than being rejected cleanly: - a space raises http.client.InvalidURL before the request is sent, which surfaces to callers as a confusing error instead of a 404 - "?" or "#" in an id truncates the path, so the server sees a different resource - "/" in an id adds path segments - "+" in an embed token's user_email decodes server-side as a space, and "&" in any query value injects another parameter Adds api.build_url, which fills a template with each value quoted as a single path segment (safe=""), and api.build_query, which urlencodes query params and skips None. Converts all 42 path call sites and all 5 query-string sites. Ordinary Felt ids — short slugs and UUIDs — contain no characters that need encoding, so request URLs are byte-identical for every existing caller. Adds tests/url_building_test.py (14 tests, no API token needed) and registers it in tests/tests.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
Ids are interpolated straight into the URL path with
str.format, and the five query-string call sites build their query with f-strings. Neither encodes, so a value that isn't already URL-safe silently changes the request instead of being rejected cleanly.Found while stress-testing the endpoints added in #35 — passing an id with a space to
create_source_credentialraisedhttp.client.InvalidURLfrom deep inside the stdlib rather than returning a 404. The pattern is repo-wide and predates that PR; every module does it and none quote.What goes wrong today
map_id="not a real id"http.client.InvalidURLbefore any request/maps/not%20a%20real%20id→ 404map_id="abc?x=1"/maps/abc,x=1becomes a query param/maps/abc%3Fx%3D1→ 404map_id="abc#frag"#dropped/maps/abc%23frag→ 404map_id="../../sources"/maps/..%2F..%2Fsources→ 404user_email="a+b@example.com"+decodes server-side as a spacea%2Bb%40example.comsource="felt&admin=true"felt%26admin%3DtrueApproach
Two helpers in
felt_python/api.py:build_url(template, **path_params)— fills the template with each value quoted as a single path segment (safe="", so/is encoded too)build_query(url, **params)—urlencodes query params and skipsNone, replacing theif x: url = f"{url}?x={x}"patternThen a mechanical conversion of all 42 path call sites and all 5 query-string sites across 8 modules.
Compatibility
Ordinary Felt ids — short slugs like
PF0ve5FaSWujSB5402D8wDand UUIDs — contain no characters that need encoding, so request URLs are byte-identical for every existing caller. Behaviour only changes for ids that would previously have produced a wrong request or a stdlib exception.Test plan
No token needed
Expect: 14 tests, OK — covers segment encoding (space,
?,#,/, unicode), query encoding (+,&,None-skipping), and that ordinary slugs/UUIDs pass through byte-identical.With a
FELT_API_TOKEN(any workspace)A malformed id is now a clean 404 instead of a stdlib crash:
Before:
http.client.InvalidURL: URL can't contain control charactersraised before any request is sent.After:
urllib.error.HTTPError: HTTP Error 404: Not Found.An id can no longer silently escape the path. With a real map id:
Before: returns the map — the
?x=1suffix silently became a query string, so you got a different resource than the id you passed.After:
HTTP Error 404— the suffixed id is one path segment and matches nothing.Regression spot-check — one live suite that exercises rewritten call sites end-to-end (creates and deletes its own map):
Expect: passes; request URLs are byte-identical for well-formed ids, so all existing behavior is unchanged.
Already verified
All seven live suites (
maps,layers— 13 rewritten call sites, the most of any module —sources,elements,layer_groups,library,projects) pass against felt.com on this branch.ruff formatclean;ruff checkreports only the same 14 pre-existingI001/RUF022findings as unmodifiedmain.Not covered by a live test:
comments.py's three call sites and the export query string, since the repo has no comments test — the unit tests cover their URL construction.Merge order note
This branches off
main, so it does not convert the 9 call sites in #35's newcomponents.pyand source-credential functions. Whichever merges second will need those converted — happy to rebase this on top of #35 instead if you'd rather do it in one pass.🤖 Generated with Claude Code