Skip to content

feat(cache): add opt-in sort_query to merge reordered query strings - #353

Merged
allen0099 merged 1 commit into
masterfrom
feat/267-sort-query
Sep 29, 2026
Merged

allen0099 merged 1 commit into
masterfrom
feat/267-sort-query

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Closes #267

What

@cache(sort_query=True) makes ?limit=1&q=wid and ?q=wid&limit=1 share one cache entry. The default False leaves every existing key byte-for-byte unchanged.

How the query is sorted

  • The key's query component was already str(request.query_params): Starlette parses the query (blank values kept, empty && segments dropped, names and values percent-decoded) and re-encodes the pairs in the order sent. sort_query stable-sorts those same decoded (name, value) pairs by name and encodes them with the same urlencode, so only the order changes. A query that is already in order gets the identical key with or without the flag.
  • Sorting the raw segments instead would have encoded differently from the unsorted key (for example %20 against +), and would split requests that the default key already treats as equal.
  • Names are compared decoded, because the key already writes %61 as a. Sorting by the raw name would put %61=1 apart from the other a values even though the key shows them as the same name.
  • The sort is stable (key=itemgetter(0)), so ?tag=b&tag=a stays distinct from ?tag=a&tag=b: a handler reading tag: list[str] sees a different order.

Validation and other entry points

  • sort_query must be a bool. Combining it with a custom key_builder raises CacheXError when the decorator is applied (the same style as the other decoration-time checks), and the message points to build_cache_key(request, sort_query=True). build_cache_key gains that keyword-only parameter, so a custom builder can opt in itself.
  • invalidate() gains a keyword-only sort_query, validated the same way. Like key_builder and vary, it cannot read the route's settings, so the caller passes the route's value. This is documented, and a test pins that omitting it misses the sorted entry.
  • clear_path() matches on the path component only, so it is unaffected. A test covers this.

Docs

  • docs/HTTP_CACHING.md: the "query order matters" note now describes sort_query, and the invalidate() section is updated.
  • The zh-TW translation is updated to match.
  • The @cache, build_cache_key and invalidate docstrings are updated.
  • Changelog fragment changelog.d/267.added.md.

Tests

tests/test_cache_sort_query.py covers:

  • reordered parameters sharing one entry (sync and async handlers);
  • repeated-name order staying distinct;
  • the default keeping reordered queries apart;
  • only order being merged;
  • edge cases: empty query, &, &&, a segment without =, blank values;
  • percent-encoded names and values kept as they were;
  • decoded-name comparison;
  • the custom key_builder combination and non-bool values being rejected;
  • invalidate() with a reordered query;
  • clear_path().

Mutation checks:

  • Sorting by name and value (an unstable order for repeated names) fails 4 tests.
  • Ignoring the flag in the key fails 15 tests.
  • Always choosing the default builder fails 6 tests.

With @cache(sort_query=True) the default key builder stable-sorts the
query parameters by decoded name, so ?a=1&b=2 and ?b=2&a=1 share one
entry while repeated values of one name keep their order. Names and
values are encoded as in the unsorted key; the default False leaves every
key unchanged. build_cache_key() and invalidate() take the same keyword,
and combining the flag with a custom key_builder raises CacheXError.
@allen0099
allen0099 merged commit 0cafbb0 into master Sep 29, 2026
12 checks passed
@allen0099
allen0099 deleted the feat/267-sort-query branch September 29, 2026 06:53
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.

@cache: opt-in sort_query to merge reordered query strings

1 participant