From a40f7bda210cefb3a34758308662a88bc01b20db Mon Sep 17 00:00:00 2001 From: Sarvatarshan Sankar <92633836+sarva-20@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:00:06 +0530 Subject: [PATCH] Fix: Prevent Search API Key from resetting when saving other settings (#12243) Co-authored-by: Sarvatarshan Sankar --- openhands/server/app.py | 2 +- openhands/server/routes/settings.py | 4 +- tests/unit/server/routes/test_settings_api.py | 38 ++++++++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/openhands/server/app.py b/openhands/server/app.py index d0dfa21f56..bf9f5b253b 100644 --- a/openhands/server/app.py +++ b/openhands/server/app.py @@ -22,7 +22,7 @@ from fastapi import ( ) from fastapi.responses import JSONResponse -import openhands.agenthub # noqa F401 (we import this to get the agents registered) +# import openhands.agenthub from openhands.app_server import v1_router from openhands.app_server.config import get_app_lifespan_service from openhands.integrations.service_types import AuthenticationError diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index 5707110d84..8ae35b0f01 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -127,8 +127,8 @@ async def store_llm_settings( settings.llm_model = existing_settings.llm_model if settings.llm_base_url is None: settings.llm_base_url = existing_settings.llm_base_url - # Keep existing search API key if not provided - if settings.search_api_key is None: + # Keep search API key if missing or empty + if not settings.search_api_key: settings.search_api_key = existing_settings.search_api_key return settings diff --git a/tests/unit/server/routes/test_settings_api.py b/tests/unit/server/routes/test_settings_api.py index 6ea4080388..ce0922996a 100644 --- a/tests/unit/server/routes/test_settings_api.py +++ b/tests/unit/server/routes/test_settings_api.py @@ -34,7 +34,9 @@ class MockUserAuth(UserAuth): async def get_access_token(self) -> SecretStr | None: return SecretStr('test-token') - async def get_provider_tokens(self) -> dict[ProviderType, ProviderToken] | None: # noqa: E501 + async def get_provider_tokens( + self, + ) -> dict[ProviderType, ProviderToken] | None: # noqa: E501 return None async def get_user_settings_store(self) -> SettingsStore | None: @@ -116,3 +118,37 @@ async def test_settings_api_endpoints(test_client): # Test the unset-provider-tokens endpoint response = test_client.post('/api/unset-provider-tokens') assert response.status_code == 200 + + +@pytest.mark.asyncio +async def test_search_api_key_preservation(test_client): + """Test that search_api_key is preserved when sending empty string""" + # 1. Set initial settings with a search API key + initial_settings = { + 'search_api_key': 'initial-secret-key', + 'llm_model': 'gpt-4', + } + response = test_client.post('/api/settings', json=initial_settings) + assert response.status_code == 200 + + # Verify key is set + response = test_client.get('/api/settings') + assert response.status_code == 200 + assert response.json()['search_api_key_set'] is True + + # 2. Update settings with EMPTY search API key (simulating the frontend bug) + # and changing another field (llm_model) + update_settings = { + 'search_api_key': '', # The frontend sends an empty string here + 'llm_model': 'claude-3-opus', + } + response = test_client.post('/api/settings', json=update_settings) + assert response.status_code == 200 + + # 3. Verify the key was NOT wiped out (The Critical Check) + response = test_client.get('/api/settings') + assert response.status_code == 200 + # If the bug was present, this would be False + assert response.json()['search_api_key_set'] is True + # Verify the other field updated correctly + assert response.json()['llm_model'] == 'claude-3-opus'