Skip to content

Shared httpx.Client causes base_url clobber between PostgREST and Storage - results in 404 and KeyError #1244

@jwstanly

Description

@jwstanly

Describe the bug

When passing a custom httpx.Client via SyncClientOptions(httpx_client=...), the same client instance is shared across all Supabase services (PostgREST, Storage, Auth, Functions). Each service mutates the httpx.Client.base_url when initialized, causing subsequent requests to other services to hit the wrong API endpoints.

Symptoms:

  • 404 errors when Storage methods hit /rest/v1/object/list/... instead of /storage/v1/object/list/...
  • Cryptic KeyError: 'error' because the 404 response format differs between PostgREST and Storage APIs
  • Non-deterministic failures depending on which service was accessed most recently

Root Cause:

The issue is in how storage3 and postgrest clients handle injected httpx_client:

storage3/_sync/client.py:77-80

if http_client is not None:
    http_client.base_url = base_url  # Mutates shared client!
    http_client.headers.update({**headers})
    return http_client

Timeline:

  1. Storage service initialized → httpx_client.base_url = ".../storage/v1/" (Works)
  2. PostgREST service accessed → httpx_client.base_url = ".../rest/v1/" Clobbers storage URL
  3. Storage methods called → still points to /rest/v1/ → 404 → KeyError: 'error'

To Reproduce

Steps to reproduce the behavior:

import httpx
from supabase import Client, create_client
from supabase.lib.client_options import SyncClientOptions

url = "https://your-project.supabase.co"
key = "your-anon-key"

# Create client with shared httpx instance
timeout = httpx.Timeout(10.0, read=60.0)
httpx_client = httpx.Client(timeout=timeout)
options = SyncClientOptions(httpx_client=httpx_client)
client: Client = create_client(url, key, options=options)

bucket = client.storage.from_("your-bucket")
print(f"Initial base_url: {bucket._client.base_url}")
# Output: https://your-project.supabase.co/storage/v1/

# First storage call works
bucket.list("some-path/")  # Works

# Touch PostgREST (causes base_url mutation)
_ = client.postgrest
print(f"After postgrest: {bucket._client.base_url}")
# Output: https://your-project.supabase.co/rest/v1/  WRONG!

# Second storage call fails
bucket.list("some-path/")  # KeyError: 'error'

Expected behavior

Storage requests should always hit /storage/v1/ endpoints regardless of whether PostgREST or other services have been accessed. The second bucket.list() call should succeed with the same results as the first call.

Actual behavior: After accessing PostgREST, Storage requests hit /rest/v1/ endpoints, causing 404 errors and KeyError: 'error' exceptions.

Screenshots

Terminal output demonstrating the bug:

# Demonstrating base_url mutation
bucket = client.storage.from_("snapshots")
print(f"Initial base_url: {bucket._client.base_url}")
# Output: https://your-project.supabase.co/storage/v1/

bucket.list("some-path/")  # Works

_ = client.postgrest  # Touch PostgREST
print(f"After accessing postgrest: {bucket._client.base_url}")
# Output: https://your-project.supabase.co/rest/v1/  CLOBBERED!

bucket.list("some-path/")  # Fails with KeyError: 'error'

Error output:

KeyError: 'error'
  File "storage3/_sync/file_api.py", line 53, in _request
    response.raise_for_status()
  File "httpx/_models.py", line 829, in raise_for_status
    raise HTTPStatusError(message, request=request, response=self)
httpx.HTTPStatusError: Client error '404 Not Found' for url 'https://your-project.supabase.co/rest/v1/object/list/snapshots'

During handling of the above exception, another exception occurred:
  File "storage3/_sync/file_api.py", line 56, in _request
    raise StorageApiError(resp["message"], resp["error"], resp["statusCode"])
KeyError: 'error'

System information

  • OS: macOS 14.5 / Linux (Ubuntu 22.04)
  • Version of supabase-py: 2.x (latest)
  • Version of storage3: 0.x (latest)
  • Version of Python: 3.12.9
  • httpx version: (latest)

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions