Skip to content

fix(http-runtime): percent-encode every reserved character in query items - #1270

Open
grdsdev wants to merge 4 commits into
mainfrom
guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model
Open

grdsdev wants to merge 4 commits into
mainfrom
guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model

Conversation

@grdsdev

@grdsdev grdsdev commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes SDK-1564, but not as originally scoped. Stacked on #1267.

Why

SDK-1564 asked for a new PostgrestRequest value model. Spec §4.7 says the opposite — reuse HTTPRuntime, don't invent a transport. HTTPRuntime already covers everything the task needed: HTTPRequest, HTTPRequestBuilder.addQuery (repeated keys), Prefer-directive merging, and request inspection via HTTPTransportStub. So PostgrestRequest is dropped; what's left here is the one real bug in HTTPRuntime.

The fix

HTTPRequestBuilder.build() used URLComponents.queryItems, whose encoding leaves + literal — a form-decoding server reads that as a space, silently corrupting timestamps and phone numbers. QueryEncoding now percent-encodes everything RFC 3986 reserves except ?// (permitted inside a query, and kept for log readability).

No production code imports HTTPRuntime yet, so there's no shipped behavior to preserve — PostgREST v3 is its first consumer.

Also included

  • setQuery: replaces a single-valued query item in place instead of appending. Needed for SDK-1568 — select, order, limit, etc. should be set once, not accumulated.
  • Reopens SDK-1565: the plan picked a thin public PostgrestTransport over the spec's recommended reuse — same call this PR made, worth revisiting together.

Tests

30 of PostgREST's 32 recorded wire-format snapshots, re-expressed against HTTPRequestBuilder and asserted against the real URLRequest.url — not the snapshot's .curl string, which sorts and re-encodes the query and would hide this exact bug. All 30 match the legacy builder byte-for-byte.

Verification

  • swift test — full suite passes
  • ./scripts/format.sh, ./scripts/spell-check.sh — clean
  • No public API change: HTTPRuntime is package-only, so no sdk-compliance.yaml / migration entry needed

@grdsdev
grdsdev requested a review from a team as a code owner August 22, 2026 10:13
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b0087f4-fe81-405b-bcde-bb61b25552bd

📥 Commits

Reviewing files that changed from the base of the PR and between ee53a2f and 3b18c78.

📒 Files selected for processing (2)
  • Sources/HTTPRuntime/HTTPRequest.swift
  • Tests/HTTPRuntimeTests/QueryEncodingTests.swift

Included review availability: Your plan provides up to 4 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for adding, replacing, and preserving the order of URL query parameters.
    • Improved query-string encoding for special characters, repeated parameters, valueless items, and non-ASCII text.
    • Prevented unnecessary trailing question marks when no query parameters are present.
  • Bug Fixes

    • Ensured query values are encoded correctly without double-encoding.
  • Documentation

    • Updated legacy guidance to reference the newer HTTP request builder.

Walkthrough

This change adds QueryEncoding for explicit RFC 3986 query encoding. HTTPRequestBuilder gains setQuery for in-place replacement or appending. build() assigns rendered queries to percentEncodedQuery. Tests cover encoding rules, query mutation, repeated items, nil values, and 30 recorded PostgREST wire-format cases. The legacy README now references HTTPRequestBuilder and the HTTPRuntime request model.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant HTTPRequestBuilder
  participant QueryEncoding
  participant URLComponents
  Caller->>HTTPRequestBuilder: setQuery name and value
  HTTPRequestBuilder->>HTTPRequestBuilder: replace or append query item
  Caller->>HTTPRequestBuilder: build request
  HTTPRequestBuilder->>QueryEncoding: render query items
  QueryEncoding-->>HTTPRequestBuilder: encoded query string
  HTTPRequestBuilder->>URLComponents: set percentEncodedQuery
Loading

Merge Risk: ⚪ Minimal · up to 3b18c

The PR tightens query encoding and adds localized query replacement behavior without introducing current production callers, new dependencies, or identified correctness, security, or availability issues. No actionable merge-blocking risk remains beyond normal checks and review.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from 72c03ec to b4a9955 Compare August 22, 2026 10:22
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

⚠️ Capability matrix drift detected

The following capabilities are marked implemented in the matrix but could not be found in swift:

  • client.session_management.persist_session → expected symbol: AuthLocalStorage.defaultLocalStorage
  • functions.invocation.streaming_response → expected symbol: FunctionsClient._invokeWithStreamedResponse

The following capabilities are marked implemented in swift but have no registered symbols to verify:

  • auth.passkey.register_passkey (no symbols list — cannot confirm implementation exists)
  • auth.passkey.sign_in_with_passkey (no symbols list — cannot confirm implementation exists)
  • client.observability.trace_propagation (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.request_cancellation (no symbols list — cannot confirm implementation exists)
  • functions.invocation.request_cancellation (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.url_cache_nonce (no symbols list — cannot confirm implementation exists)

These may have been renamed, removed, or never registered. Please update the capability matrix.
See: https://github.com/supabase/sdk/blob/main/packages/capability-matrix/docs/capability-matrix.md

@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from b4a9955 to b78e635 Compare August 22, 2026 10:38
@grdsdev grdsdev changed the title feat(postgrest): add the PostgrestRequest value model fix(http-runtime): percent-encode every reserved character in query items Aug 22, 2026
@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from 3a15b20 to 051134d Compare August 24, 2026 08:32
@coveralls

coveralls commented Aug 24, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 33060279490

Coverage decreased (-0.2%) to 86.941%

Details

  • Coverage decreased (-0.2%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 17 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

17 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift 17 63.98%

Coverage Stats

Coverage Status
Relevant Lines: 10613
Covered Lines: 9227
Line Coverage: 86.94%
Coverage Strength: 1011900.89 hits per line

💛 - Coveralls

@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from 051134d to f8a62d6 Compare August 24, 2026 08:38
@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from f8a62d6 to 00fc225 Compare August 25, 2026 08:19
@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from 00fc225 to 57f3d11 Compare August 25, 2026 08:46
@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from 57f3d11 to 5654169 Compare August 25, 2026 09:08
@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from 5654169 to 3c6554b Compare August 25, 2026 09:17
Base automatically changed from guilhermesouza/sdk-1563-refactorpostgrest-move-todays-builders-under-legacy to main August 25, 2026 09:25
@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from 3c6554b to ee53a2f Compare August 25, 2026 09:25
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Potential Breaking API Changes Detected

This PR appears to contain breaking API changes. Please review the changes below:

API Check Output

If this is intentional, please update your PR title or commit message to include:

  • ! after the type (e.g., feat!: remove deprecated method)
  • Or include BREAKING CHANGE: in the commit body

If this is a false positive, you can safely ignore this warning.

…tems

`HTTPRequestBuilder.build()` assigned `URLComponents.queryItems`, whose
encoding leaves `+` literal. A server that form-decodes the query string
reads a literal `+` as a space, so

    received_at=gt.2023-03-23T15:50:30.511743+00:00
    to=eq.+16505555555

arrive with a space where the UTC offset should be, and without a country
code. Neither fails loudly; both just match the wrong rows.

`QueryEncoding` escapes everything RFC 3986 reserves, keeping only `?` and
`/`, which section 3.4 permits inside a query — leaving those readable in a
log without changing how they parse. It renders the whole query in one pass
so escaping happens exactly once, at build time, on both names and values.
Items are still stored unescaped, so an existing `%` cannot be double-encoded.

This is the same character set `Helpers`' `sbURLQueryAllowed` already applies
on PostgREST's legacy path, which is what makes the port below an equality
rather than an approximation.

Nothing in production imports `HTTPRuntime` yet — only its own tests and
`HTTPRuntimeTestHelpers` — so there is no shipped behavior to preserve.
PostgREST v3 is its first consumer, and the generated clients inherit the fix
before they land.

13 tests, including 30 of the 32 requests PostgREST records under
`Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/`, re-expressed
against `HTTPRequestBuilder`. Those are the most varied real query values in
the repository: JSON objects, range literals, `like` patterns, a timestamptz
offset, a non-ASCII string, a leading `+`. All 30 now build byte-identical
URLs to the legacy builder.

The expected strings are not copied from the snapshot files. Those are
written by swift-snapshot-testing's `.curl` strategy, which sorts the query
items by name and re-encodes them through `URLComponents.queryItems` — so
`select=%2A` is recorded as `select=*` and insertion order is lost. Each
string is the real `URLRequest.url` the legacy builder produces, captured by
running the same chains through a fetch handler that prints it.

Two cases are tested separately. The 25-operator case reads better generated
than transcribed. And `rpc call with get and params` cannot be an exact-order
assertion at all: `PostgrestClient.rpc(_:params:get:)` iterates a
`JSONValue` object, so it emits `index=2&array=…` on one run and
`array=…&index=2` on the next — two captures of the same chain disagreed.
The snapshot never caught it because `.curl` sorts before recording.
Parameter order does not change what PostgREST returns, so it is not a wire
bug, but it does make the request unreproducible in a log or a cache key.

`HTTPRuntime` is a target, not a library product, and every symbol in it is
`package`. No public API changes, so no compliance or migration entry.
The note named `PostgrestRequest` as what replaces `PostgrestRequestBuilder`.
That type is not being built — spec §4.7 says to reuse `HTTPRuntime` rather
than invent a parallel request model, and `HTTPRequestBuilder` already
provides the ordered repeated-key query encoding it was meant to add.
`addQuery` always appends, which is right for lists (`?k=a&k=b`) and wrong
for the parameters a server reads once. PostgREST v3 needs `select`, `order`,
`limit`, `offset`, `on_conflict` and `columns` set rather than accumulated,
or a chain that touches one twice leaves two behind and lets the server pick.

`setQuery` replaces the first item using that name, in place, so the items
around it keep their position. Only the first match: a repeated name is
either a list or, in PostgREST's case, a conjunction on one column, and
replacing every match would silently collapse `id=gt.1&id=lt.9` into one
condition. A `nil` value is ignored, matching `addQuery`, so the same
argument behaves the same way in both.

4 tests, including the repeated-name case and the in-place ordering.
…ttp-types rebase

Rebasing onto main pulled in #1282's HTTPRequest/HTTPMethod → swift-http-types
migration, which QueryEncodingTests predates: `HTTPMethod` no longer exists,
`HTTPRequest.Method`/`.url` need their defining modules imported directly, and
`.url` is now optional (HTTPTypesFoundation's computed property), matching the
pattern already used in HTTPRequestBuilderTests.
@grdsdev
grdsdev force-pushed the guilhermesouza/sdk-1564-featpostgrest-add-the-postgrestrequest-value-model branch from ee53a2f to 3b18c78 Compare August 27, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants