From 017d758a76cb6ac8b10fd5b1c5c0bbf9c0a26e29 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 18 Mar 2026 14:04:46 +0000 Subject: [PATCH] fix: skip frozen fields when applying settings payload The secrets_store field on Settings is frozen, so setattr() raised a validation error when the POST /api/settings payload included that key. Filter out any frozen field before calling setattr in _apply_settings_payload. Added a regression test. Co-authored-by: openhands --- openhands/server/routes/settings.py | 7 ++++++- tests/unit/server/routes/test_settings_api.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index e312c5e7fa..a9f327692b 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -104,6 +104,11 @@ def _extract_sdk_settings_values( return values +_SETTINGS_FROZEN_FIELDS = frozenset( + name for name, field_info in Settings.model_fields.items() if field_info.frozen +) + + def _apply_settings_payload( payload: dict[str, Any], existing_settings: Settings | None, @@ -116,7 +121,7 @@ def _apply_settings_payload( sdk_settings_values = dict(settings.sdk_settings_values) for key, value in payload.items(): - if key in Settings.model_fields: + if key in Settings.model_fields and key not in _SETTINGS_FROZEN_FIELDS: setattr(settings, key, value) if key in sdk_field_keys and key not in secret_field_keys: diff --git a/tests/unit/server/routes/test_settings_api.py b/tests/unit/server/routes/test_settings_api.py index 3a2dd58a1a..e5fc5290a5 100644 --- a/tests/unit/server/routes/test_settings_api.py +++ b/tests/unit/server/routes/test_settings_api.py @@ -270,6 +270,21 @@ async def test_settings_api_endpoints(test_client): assert response.status_code == 200 +@pytest.mark.asyncio +async def test_saving_settings_with_frozen_secrets_store(test_client): + """Regression: POSTing settings must not fail when the payload includes + ``secrets_store`` (a frozen field on the Settings model). + See https://github.com/OpenHands/OpenHands/issues/13306. + """ + settings_data = { + 'language': 'en', + 'llm_model': 'gpt-4', + 'secrets_store': {'provider_tokens': {}}, + } + response = test_client.post('/api/settings', json=settings_data) + 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 an empty string."""