feat(functions): fix invoke option leakage, add method and bytes body support - #1600
Open
grdsdev wants to merge 3 commits into
Open
feat(functions): fix invoke option leakage, add method and bytes body support#1600grdsdev wants to merge 3 commits into
grdsdev wants to merge 3 commits into
Conversation
`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>
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>
Contributor
|
The following capabilities are marked
The following capabilities are marked
These may have been renamed, removed, or never registered. Please update the capability matrix. |
o-santi
approved these changes
Sep 3, 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.
What
Four fixes to
supabase_functions, backported from thev3branch 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
invokeoptions leaked onto the client (bug).invokedidheaders = self.headers, which aliases rather than copies. The subsequentheaders.update(invoke_options["headers"]),headers["x-region"], andheaders["Content-Type"]writes all landed on the client's default headers. Every laterinvoke()on the same client carried the previous call's headers, region, and content type._requestnow 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 raiseJSONDecodeError, masking the actual HTTP failure. Newerror_message_fromhelper inerrors.pyfalls back to the response text.3.
invokeaccepts amethod. Previously hard-coded to POST.invoke_options["method"]now works, defaulting to"POST", matching supabase-js'sFunctionInvokeOptions.4.
invokeaccepts abytesbody, sent asapplication/octet-stream. String and bytes bodies now go through httpx'scontent=rather than the deprecateddata=.Why
Found while auditing
v3for changes that could land onmainwithout breaking the public API.v3is overwhelmingly a structural refactor (dropunasync, unify sync/async through a generator-basedHttpIO), 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
_syncis generated, so all source edits are in_async/functions_client.pyand regenerated viamake -C src/functions build-sync.New tests in
tests/_async/test_function_client.py(mirrored into_sync):invoke(), nor toclient.headersmethodis honoured for GET/PUT/PATCH/DELETE, and defaults to POSTapplication/octet-streamand is passed ascontent=content=New parametrised test in
tests/test_errors.pycoverserror_message_fromacross 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
HTTPErrorwithout an attached request.Risk
Low. The one behavioural change users could depend on is the header leak itself — code that set a header via
invoke_optionsonce and relied on it sticking for later calls will now need to pass it to the client constructor orset_auth. That reliance was on a bug, and the leak also carriedx-regionandContent-Typeacross calls, which is far more likely to have caused problems than solved them.Notes for reviewers
make -C src/functions build-syncuses GNUsed -isyntax and fails on macOS (BSD sed). I applied the three post-unasync substitutions manually to get an identical result. Worth fixing separately.v3, all breaking: the auth type renames, the postgrest builder rewrite, the storageStorageApiErrordataclass rewrite, the realtime callback → async-iterator rewrite, and dropping Python 3.9.base_request_builder.py:102checkshttp_method == "HTTP"where the docstring says HEAD, so HEAD requests never retry. Present on bothmainandv3; out of scope here.