Skip to content

feat(functions): fix invoke option leakage, add method and bytes body support - #1600

Open
grdsdev wants to merge 3 commits into
mainfrom
claude/v3-backward-compatible-changes-203101
Open

feat(functions): fix invoke option leakage, add method and bytes body support#1600
grdsdev wants to merge 3 commits into
mainfrom
claude/v3-backward-compatible-changes-203101

Conversation

@grdsdev

@grdsdev grdsdev commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What

Four fixes to supabase_functions, backported from the v3 branch where they were incidental to the sync/async refactor. All are backward compatible — no existing call site changes behaviour except the bug fix below.

1. Per-call invoke options leaked onto the client (bug).
invoke did headers = self.headers, which aliases rather than copies. The subsequent headers.update(invoke_options["headers"]), headers["x-region"], and headers["Content-Type"] writes all landed on the client's default headers. Every later invoke() on the same client carried the previous call's headers, region, and content type.

_request now builds the merged dict itself — client defaults first, per-call headers second — so per-call values still take precedence and neither dict is mutated.

2. Non-JSON error bodies raised the wrong exception.
Both the HTTP error and relay error paths called response.json().get("error"). An edge function returning a plain-text or empty body made that raise JSONDecodeError, masking the actual HTTP failure. New error_message_from helper in errors.py falls back to the response text.

3. invoke accepts a method. Previously hard-coded to POST. invoke_options["method"] now works, defaulting to "POST", matching supabase-js's FunctionInvokeOptions.

4. invoke accepts a bytes body, sent as application/octet-stream. String and bytes bodies now go through httpx's content= rather than the deprecated data=.

Why

Found while auditing v3 for changes that could land on main without breaking the public API. v3 is overwhelmingly a structural refactor (drop unasync, unify sync/async through a generator-based HttpIO), but these four carried real user-facing value and are independent of that refactor.

Item 1 is the one worth reviewing closely — it silently corrupts a long-lived client, and the corruption grows with each call.

Test plan

_sync is generated, so all source edits are in _async/functions_client.py and regenerated via make -C src/functions build-sync.

New tests in tests/_async/test_function_client.py (mirrored into _sync):

  • per-call headers, region, and content type do not persist to the next invoke(), nor to client.headers
  • per-call headers override client defaults for the same key
  • method is honoured for GET/PUT/PATCH/DELETE, and defaults to POST
  • bytes body sets application/octet-stream and is passed as content=
  • string body is passed as content=
  • HTTP error with a non-JSON body surfaces the response text; with an empty body, the generic fallback message
  • relay error with a non-JSON body surfaces the response text

New parametrised test in tests/test_errors.py covers error_message_from across dict-with-error, dict-without-error, non-dict JSON, plain text, and empty body.

Edge cases considered: empty body, non-dict JSON payload, header key collisions between client and per-call, repeated invokes on one client, and HTTPError without an attached request.

94 passed, 1 skipped
mypy: Success: no issues found in 16 source files
ruff check + format: clean
coverage: supabase_functions 98% (the 2 misses are pre-existing)

Risk

Low. The one behavioural change users could depend on is the header leak itself — code that set a header via invoke_options once and relied on it sticking for later calls will now need to pass it to the client constructor or set_auth. That reliance was on a bug, and the leak also carried x-region and Content-Type across calls, which is far more likely to have caused problems than solved them.

Notes for reviewers

  • make -C src/functions build-sync uses GNU sed -i syntax and fails on macOS (BSD sed). I applied the three post-unasync substitutions manually to get an identical result. Worth fixing separately.
  • Not included from v3, all breaking: the auth type renames, the postgrest builder rewrite, the storage StorageApiError dataclass rewrite, the realtime callback → async-iterator rewrite, and dropping Python 3.9.
  • Separately: base_request_builder.py:102 checks http_method == "HTTP" where the docstring says HEAD, so HEAD requests never retry. Present on both main and v3; out of scope here.

grdsdev and others added 2 commits September 1, 2026 07:39
`invoke` aliased `self.headers` instead of copying it, so the per-call
`headers`, `x-region`, and `Content-Type` writes mutated the client's
default headers. Every later `invoke()` on the same client then carried
the previous call's headers and region.

Build the merged header dict in `_request` instead: client defaults
first, per-call headers second, so per-call values still win and neither
dict is mutated.

Also tolerate a non-JSON error body. Both the HTTP and relay error paths
called `response.json().get("error")`, which raised `JSONDecodeError` on
a plain-text or empty edge function response and masked the real HTTP
error. `error_message_from` falls back to the response text.

Co-Authored-By: Claude <noreply@anthropic.com>
`invoke` hard-coded POST. Accept a `method` key in `invoke_options`,
defaulting to POST, matching supabase-js's `FunctionInvokeOptions`.

Accept a `bytes` body as well, sent as `application/octet-stream`.
String and bytes bodies now go through httpx's `content=` rather than
the deprecated `data=`.

Co-Authored-By: Claude <noreply@anthropic.com>
@grdsdev
grdsdev requested review from a team and o-santi as code owners September 1, 2026 10:40
The capability matrix check flags `error_message_from` as new public API.
It is an internal helper, not a capability, so prefix it with `_` rather
than register it in sdk-compliance.yaml.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 1, 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 python:

  • database.configuration.auto_retry → expected symbol: AsyncSelectRequestBuilder.retry
  • database.configuration.auto_retry → expected symbol: SyncSelectRequestBuilder.retry
  • realtime.client.connect → expected symbol: SyncRealtimeClient.connect
  • realtime.client.disconnect → expected symbol: AsyncRealtimeClient.disconnect
  • realtime.client.disconnect → expected symbol: SyncRealtimeClient.disconnect
  • realtime.channel.subscribe → expected symbol: SyncRealtimeChannel.subscribe
  • realtime.channel.unsubscribe → expected symbol: SyncRealtimeChannel.unsubscribe
  • realtime.channel.broadcast → expected symbol: SyncRealtimeChannel.send_broadcast
  • realtime.subscriptions.broadcast → expected symbol: SyncRealtimeChannel.on_broadcast
  • realtime.subscriptions.postgres_changes → expected symbol: SyncRealtimeChannel.on_postgres_changes
  • realtime.subscriptions.subscribe_presence → expected symbol: AsyncRealtimeChannel.on_presence_change
  • realtime.subscriptions.subscribe_presence → expected symbol: SyncRealtimeChannel.on_presence_change
  • realtime.presence.track → expected symbol: SyncRealtimeChannel.track
  • realtime.presence.untrack → expected symbol: SyncRealtimeChannel.untrack
  • realtime.presence.presence_state → expected symbol: AsyncRealtimeChannel.presences
  • realtime.presence.presence_state → expected symbol: SyncRealtimeChannel.presences

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

  • auth.session.auto_refresh (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.cross_client_token_sync (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.oauth_flow_type (no symbols list — cannot confirm implementation exists)
  • client.session_management.custom_storage (no symbols list — cannot confirm implementation exists)
  • client.session_management.persist_session (no symbols list — cannot confirm implementation exists)
  • client.request_configuration.custom_http_client (no symbols list — cannot confirm implementation exists)
  • client.request_configuration.global_headers (no symbols list — cannot confirm implementation exists)
  • database.mutate.select_after_mutation (no symbols list — cannot confirm implementation exists)
  • database.using_filters.eq (no symbols list — cannot confirm implementation exists)
  • database.using_filters.neq (no symbols list — cannot confirm implementation exists)
  • database.using_filters.gt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.gte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.lt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.lte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.like (no symbols list — cannot confirm implementation exists)
  • database.using_filters.ilike (no symbols list — cannot confirm implementation exists)
  • database.using_filters.is (no symbols list — cannot confirm implementation exists)
  • database.using_filters.in (no symbols list — cannot confirm implementation exists)
  • database.using_filters.contains (no symbols list — cannot confirm implementation exists)
  • database.using_filters.contained_by (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_gt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_gte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_lt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_lte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_adjacent (no symbols list — cannot confirm implementation exists)
  • database.using_filters.overlaps (no symbols list — cannot confirm implementation exists)
  • database.using_filters.text_search (no symbols list — cannot confirm implementation exists)
  • database.using_filters.match (no symbols list — cannot confirm implementation exists)
  • database.using_filters.not (no symbols list — cannot confirm implementation exists)
  • database.using_filters.or (no symbols list — cannot confirm implementation exists)
  • database.using_filters.raw (no symbols list — cannot confirm implementation exists)
  • database.using_filters.regex (no symbols list — cannot confirm implementation exists)
  • database.using_filters.regex_icase (no symbols list — cannot confirm implementation exists)
  • database.using_filters.not_in (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.order (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.limit (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.range (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.request_cancellation (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.relationship_embed (no symbols list — cannot confirm implementation exists)
  • database.configuration.request_timeout (no symbols list — cannot confirm implementation exists)
  • functions.invocation.region_selection (no symbols list — cannot confirm implementation exists)
  • functions.invocation.request_cancellation (no symbols list — cannot confirm implementation exists)
  • functions.invocation.timeout (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.postgres_changes_filter (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.private_channel (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.broadcast_self (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.broadcast_replay (no symbols list — cannot confirm implementation exists)
  • realtime.presence.presence_key (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.heartbeat_interval (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.access_bucket (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

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