From 93972a62f73d9cf2a64683c81ad07605703882bb Mon Sep 17 00:00:00 2001 From: CC Worker Date: Sat, 6 Jun 2026 19:30:36 +0000 Subject: [PATCH] fix: revert explicit apikey header (caused Kong duplicate-apikey 401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit added apikey to _create_base_client headers, but supabase-py already sets apikey from the key arg → two apikey headers → Kong rejected every as-user call with 401 'Duplicate API key found' (exam API 502'd on auth). Revert to Authorization-only; fix the two header unit tests to assert the real contract (apikey via the key arg; options.headers carries only the user Authorization). Co-Authored-By: Claude Opus 4.8 --- modules/database/supabase/utils/client.py | 9 ++++----- tests/test_me_bootstrap.py | 8 ++++---- tests/test_p0_api_security.py | 4 +++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/modules/database/supabase/utils/client.py b/modules/database/supabase/utils/client.py index fe55f33..976f46f 100644 --- a/modules/database/supabase/utils/client.py +++ b/modules/database/supabase/utils/client.py @@ -24,12 +24,11 @@ def _create_base_client(url: str, key: str, access_token: Optional[str] = None, # Otherwise fall back to the API key auth_header = f"Bearer {access_token}" if access_token else f"Bearer {key}" - # apikey is required by the Supabase gateway (Kong) on every request and is independent of - # Authorization: for a per-user client apikey stays the anon key while Authorization carries - # the user's JWT (so RLS sees auth.uid()). Set it explicitly rather than relying on - # create_client's internal default-header behaviour, which our options.headers override. + # Only override Authorization here. apikey is supplied to create_client via the `key` arg and + # set by supabase-py itself; setting it again here sends a DUPLICATE apikey header that the + # Supabase gateway (Kong) rejects with 401 "Duplicate API key found". For a per-user client + # apikey stays the anon key (from `key`) while this Authorization carries the user JWT. headers = { - "apikey": key, "Authorization": auth_header, } if options: diff --git a/tests/test_me_bootstrap.py b/tests/test_me_bootstrap.py index b1ca15f..88273cf 100644 --- a/tests/test_me_bootstrap.py +++ b/tests/test_me_bootstrap.py @@ -122,11 +122,11 @@ def test_supabase_client_for_user_uses_access_token_authorization(monkeypatch): assert anon.access_token == "user-token" assert captured["url"] == "http://supabase.test" + # apikey is supplied via the `key` positional arg (supabase-py sets the apikey header from it). + # options.headers must carry ONLY the per-user Authorization override — adding apikey here too + # produces a duplicate apikey header that Kong rejects ("Duplicate API key found"). assert captured["key"] == "anon-key" - assert captured["options_kwargs"]["headers"] == { - "apikey": "anon-key", - "Authorization": "Bearer user-token", - } + assert captured["options_kwargs"]["headers"] == {"Authorization": "Bearer user-token"} def test_no_school_bootstrap_requires_school_membership_but_allows_canvas(): diff --git a/tests/test_p0_api_security.py b/tests/test_p0_api_security.py index 5914e92..d38b4c2 100644 --- a/tests/test_p0_api_security.py +++ b/tests/test_p0_api_security.py @@ -26,8 +26,10 @@ def test_supabase_anon_for_user_sets_user_authorization_header(monkeypatch): client_module.SupabaseAnonClient.for_user('Bearer user-jwt') + # apikey comes from the `key` arg (supabase-py sets the apikey header); options.headers must + # carry only the user Authorization override. A second apikey here → Kong "Duplicate API key". assert captured['key'] == 'anon-key' - assert captured['options'].headers['apikey'] == 'anon-key' + assert 'apikey' not in captured['options'].headers assert captured['options'].headers['Authorization'] == 'Bearer user-jwt'