From e96e337d794fced8caa6c6a6f7420470ef1a3579 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 8 Oct 2026 23:10:43 -0700 Subject: [PATCH] fix(credentials): answer 405 with Allow: GET for PATCH and DELETE on config credentials A config-defined credential exists (GET returns it) but is read-only through the API, which is what 405 Method Not Allowed means. 400 described a malformed request, and 404 would claim the credential does not exist. --- litellm/proxy/credential_endpoints/endpoints.py | 3 ++- tests/unit/proxy/credential_endpoints/test_endpoints.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/credential_endpoints/endpoints.py b/litellm/proxy/credential_endpoints/endpoints.py index 09d9fcedb3b..8df22f458cf 100644 --- a/litellm/proxy/credential_endpoints/endpoints.py +++ b/litellm/proxy/credential_endpoints/endpoints.py @@ -181,8 +181,9 @@ def _not_found_unless_config_defined(credential_name: str, not_found_detail: str return ProxyException( message=f"Credential '{credential_name}' is defined in config and cannot be edited from the API or UI.", type=ProxyErrorTypes.validation_error.value, - code=status.HTTP_400_BAD_REQUEST, + code=status.HTTP_405_METHOD_NOT_ALLOWED, param="credential_name", + headers={"Allow": "GET"}, ) diff --git a/tests/unit/proxy/credential_endpoints/test_endpoints.py b/tests/unit/proxy/credential_endpoints/test_endpoints.py index 9c94b47b8f4..83d6167256a 100644 --- a/tests/unit/proxy/credential_endpoints/test_endpoints.py +++ b/tests/unit/proxy/credential_endpoints/test_endpoints.py @@ -999,7 +999,8 @@ class TestNonAdminCannotTouchAStoredWifCredential: with _repository_holding(None) as repository: response = _delete_credential("config-wif", auth=_as_admin) - assert response.status_code == 400, response.text + assert response.status_code == 405, response.text + assert response.headers["allow"] == "GET" assert "defined in config" in response.json()["error"]["message"] repository.delete_by_name.assert_awaited_once_with("config-wif") assert litellm.credential_list == [config_credential] @@ -1198,7 +1199,8 @@ def test_delete_credential_leaves_a_credential_that_only_exists_in_memory_in_pla response = _delete_credential("from-config-yaml") - assert response.status_code == 400, response.text + assert response.status_code == 405, response.text + assert response.headers["allow"] == "GET" assert "defined in config" in response.json()["error"]["message"] assert [credential.credential_name for credential in litellm.credential_list] == ["from-config-yaml"] @@ -1526,7 +1528,8 @@ class TestCredentialDisplayName: with _repository_holding(None) as repository: response = _patch_credential("openai-prod", {"display_name": "Prod", "credential_info": {}}) - assert response.status_code == 400, response.text + assert response.status_code == 405, response.text + assert response.headers["allow"] == "GET" assert "defined in config" in response.json()["error"]["message"] repository.update_by_name.assert_not_awaited() assert litellm.credential_list == [config_credential]