refactor(api): collapse three query-value encoders into one - #111
Merged
Merged
Conversation
`.urlQueryAllowed` permits `+ & = ? # /`, so percent-encoding a query *value* with it leaves them intact: a raw `+` reaches the backend as a space (params are read via `URLSearchParams`) and a raw `&` or `=` splits the value into another query parameter. Six call sites still passed user-typed search terms and an opaque cursor through the raw set, while three separate local fixes for the same hazard had accumulated in parallel branches. Hoist one `queryValue(_:)` into `APIClientTransport.swift`, next to `pathSegment(_:)` under a shared "URL encoding" heading so the next caller sees both and picks deliberately. It is `internal`, not `private`, so the per-feature `APIClient+*.swift` extensions can reach it. The three local copies (`orgQueryValueAllowed`, `encodedQueryValue`, `queryValueAllowed`) are gone. Path encoding via `.urlPathAllowed` is untouched — different job, different allowed set. Closes #95. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1
This was referenced Sep 17, 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.
Summary
Closes #95.
CharacterSet.urlQueryAllowedpermits+,&,=,?,#and/, sopercent-encoding a query value with it leaves them intact. A raw
+reaches the backend as aspace (params are read through
URLSearchParams); a raw&or=splits the value intoanother query parameter. For a search box that silently searches for the wrong thing; for an
opaque base64 cursor, paging quietly breaks with no error.
Three independent local fixes for this had accumulated on parallel branches. Now that #86 and #93
are both on
main, all three live in one tree and collapse into a single helper.What's included
queryValue(_:)inServices/APIClientTransport.swift,internalrather thanprivateso the per-feature
APIClient+*.swiftextensions can see it (privateis file-scoped in Swift —that internal seam is deliberate, see CLAUDE.md). Allowed set is
.urlQueryAllowed.subtracting(CharacterSet(charactersIn: "+&=?#/")), and the "why" note travelswith it.
pathSegment(_:)andqueryValue(_:)now sit adjacent under a// MARK: - URL encodingheading, each documented against the other, so the next caller sees both and picks the right one
without having to reason it out. Path encoding via
.urlPathAllowedis otherwise untouched —different job, different allowed set.
orgQueryValueAllowed(APIClient+Organizations.swift),encodedQueryValue(APIClient+DirectMessages.swift),queryValueAllowed(
APIClient.swift). A grep confirms exactly one query-value encoder remains in the codebase.Call sites converted
Six raw
.urlQueryAllowedvalues inServices/APIClient.swift— exactly the count the issueestimated, though the line numbers had shifted:
searchDocuments(q:limit:offset:)searchLists(q:limit:offset:)searchWatcherCandidates(listId:…:search:)searchDocumentCollaboratorCandidates(id:query:)searchMessages(q:limit:offset:)dmThreadUpdates(username:after:)aftercursorThree sites already encoding correctly were repointed at the shared helper, no behaviour change:
unlinkIdentity(provider:),organizationUsers(id:search:excludeMembers:…)(bothsearchandexcludeMembers— the literal commas the backend splits on are still preserved), anddmConversations(cursor:take:).Testing
InterlinedListTests/APIClientTests/APIClientQueryValueEncodingTests.swift(new, hand-slotted intoproject.pbxproj) covers every converted site plus the three pre-existing ones. Each case assertsthe value round-trips unchanged through
URLComponents(...).queryItems, that it adds no extra queryparameter, and that the wire form carries no literal
+— the last one matters because a raw+survives
URLComponentsintact and only goes wrong server-side. A tenth test pins that pathsegments stay path-encoded, so the two helpers can't be quietly swapped.
c++ & a=b(contains+,&,=, space) round-trips unchanged.YWJjKz0vZGVm+/w==(contains+,/,=) round-trips unchanged.Full suite: 1330 tests, 0 failures (baseline on
mainwas 1320; +10 new), run with-parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTestson a pinnedsimulator UDID and a worktree-local DerivedData.
The tests bite. Reverting the allowed set to plain
.urlQueryAllowedand re-running fails all 9encoding tests in the new file and the 3 pre-existing ones (32 assertion failures). The 10th new
test, the path-encoding guard, correctly stays green. Helper restored and the full suite re-run
green afterwards.
Noted, not fixed (out of scope)
Path segments are still encoded inline as
id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? idat many call sites eventhough
pathSegment(_:)exists for exactly that. Correct as written, just not using the helper —worth a separate sweep.
🤖 Generated with Claude Code