Skip to content

fix(py-client): Treat object keys as literal strings#548

Merged
lcian merged 5 commits into
mainfrom
lcian/fix/py-client-literal-key-encoding
Jul 10, 2026
Merged

fix(py-client): Treat object keys as literal strings#548
lcian merged 5 commits into
mainfrom
lcian/fix/py-client-literal-key-encoding

Conversation

@lcian

@lcian lcian commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Stacked on #544. Makes the Python client encode request paths itself (via presign.encode_path) instead of relying on urllib3's implicit encoding, so a key is always treated as a literal string.

The bug

urllib3's encoder treats a literal %XX (percent + two hex digits) in a key as an existing escape and leaves it on the wire unchanged. Pre-signed URLs (which use urllib.parse.quote) instead escape the % to %25. So for a key like looks%20encoded, put/get and the pre-signed GET disagreed on which object was addressed:

path put/get (old urllib3) pre-signed GET (quote)
wire …/looks%20encoded …/looks%2520encoded
server resolves key looks encoded (space!) looks%20encoded (literal)

The fix

_make_url / _make_multipart_url now percent-encode the assembled path with presign.encode_path. Our output is a subset of urllib3's safe set and contains only valid %XX, so urllib3 transmits it verbatim — meaning wire bytes only change for keys containing a literal %XX; every other key is byte-identical to before. The whole client now treats keys as opaque literal strings, consistent with pre-signed URLs.

Rust client

Verified not affected: the Rust client builds URLs via the url crate's path_segments_mut().push(), which already escapes a literal % to %25 (looks%20encodedlooks%2520encoded). It already treats keys as literals, so no change is needed.

Tests

  • Re-enabled the looks%20encoded case in test_presigned_get_encoding_corner_cases (previously commented out because it diverged).
  • Added test_put_stores_under_literal_key, which asserts the object lands under the literal key by checking the key the server echoes in the put response body (would be looks encoded under the old behavior).

Verification

  • ruff format / ruff check / mypy: clean
  • Python unit + e2e + multipart suites: 68 passed (e2e runs against a filesystem-backed server)
  • cargo build, cargo fmt --check, cargo clippy --workspace --all-targets --all-features: clean
  • cargo test -p objectstore-client --all-features: passed

Note for reviewers

This is a wire-visible behavior change for the (pathological) case of keys containing a literal %XX: such objects are now stored/fetched under the literal key rather than urllib3's percent-decoded interpretation. Objects previously written under the old interpretation would live at a different path.

Close FS-416

@codecov

This comment has been minimized.

@lcian lcian marked this pull request as ready for review July 9, 2026 11:38
@lcian lcian requested a review from a team as a code owner July 9, 2026 11:39
@linear-code

linear-code Bot commented Jul 9, 2026

Copy link
Copy Markdown

FS-416

@lcian lcian changed the title fix(py-client): treat object keys as literal strings in request paths fix(py-client): Treat object keys as literal strings Jul 9, 2026
@lcian lcian force-pushed the lcian/fix/py-client-literal-key-encoding branch from c1edfc2 to f547282 Compare July 9, 2026 11:49
Base automatically changed from lcian/feat/presigned-urls-py-client to main July 10, 2026 08:37
lcian added 3 commits July 10, 2026 10:39
Percent-encode request paths in `_make_url`/`_make_multipart_url` with
`presign.encode_path` instead of relying on urllib3's implicit encoding.
urllib3 treats a literal `%XX` in a key as an existing escape and leaves it
on the wire, so `put`/`get`/`delete` and pre-signed URLs disagreed on which
object a `%XX` key referred to. Encoding ourselves treats the key as a
literal string everywhere (a literal `%` becomes `%25`), matching pre-signed
URLs and the Rust client (whose `url` crate already escapes `%`).

Re-enables the `looks%20encoded` encoding corner case and adds a test
asserting `put` stores under the literal key echoed in its response body.
`encode_path`/`encode_query` are general on-the-wire URL encoding, not
specific to pre-signed URLs. Move them (and the `_PATH_SAFE`/`_QUERY_SAFE`
sets) from `presign` to `utils` so `_make_url`/`_make_multipart_url` and the
pre-signing code call one shared, neutrally-located helper instead of the
request path depending on the `presign` module.
@lcian lcian force-pushed the lcian/fix/py-client-literal-key-encoding branch from f547282 to 308068d Compare July 10, 2026 08:39
@lcian lcian enabled auto-merge (squash) July 10, 2026 08:40
@lcian lcian disabled auto-merge July 10, 2026 08:40
@lcian lcian merged commit f7e76c0 into main Jul 10, 2026
27 checks passed
@lcian lcian deleted the lcian/fix/py-client-literal-key-encoding branch July 10, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants