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.
This commit is contained in:
Yuneng Jiang 2026-10-08 23:10:43 -07:00
parent 23a7e1dc29
commit e96e337d79
No known key found for this signature in database
2 changed files with 8 additions and 4 deletions

View file

@ -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"},
)

View file

@ -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]