fix(proxy): name shared-handler errors by the status they answer - #39555
Open
mateo-berri wants to merge 1 commit into
Open
mateo-berri wants to merge 1 commit into
mateo-berri wants to merge 1 commit into
Conversation
handle_exception_on_proxy pinned type=internal_server_error on every
exception it wrapped, so a management route rejecting a caller's own
argument answered {"type":"internal_server_error","code":"400"} and an
SDK branching on the type read its own bad request as a gateway outage.
Derive the type from the status the response actually carries, keeping
a type the exception already names.
Contributor
Greptile SummaryThe PR updates the shared proxy exception handler so response error types reflect the effective HTTP status while preserving explicit exception types.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/proxy/utils.py | Replaces unconditional internal-server classification with shared status-aware error normalization while retaining existing ProxyException instances. |
| tests/test_litellm/proxy/credential_endpoints/test_endpoints.py | Adds an endpoint regression test confirming a missing credential is returned as a 404 invalid-request error. |
| tests/test_litellm/proxy/utils/helpers/test_error_helpers.py | Expands unit coverage across client, authentication, permission, rate-limit, and server error classifications. |
Reviews (2): Last reviewed commit: "fix(proxy): name shared-handler errors b..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
Author
|
bugbot run |
Contributor
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6a9cb48. Configure here.
6 tasks
yucheng-berri
approved these changes
Sep 3, 2026
9 tasks
Base automatically changed from
litellm_openai_error_payload_non_llm_routes
to
litellm_internal_staging
September 8, 2026 23:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
internal_server_errorHow it solves it:
invalid_request_errorinternal_server_errorUser Flow
Before: a developer's provisioning script creates keys and teams through the gateway's management API, and every rejection of its own bad input comes back looking like the gateway is down, so the script's retry loop never stops
{"budget_duration": "not-a-duration"}{"type":"internal_server_error","code":"400"}, saying the duration format is wrongteam_id(400), on POST https://litellm-domain/user/new with a duplicateuser_id(409), and on GET https://litellm-domain/credentials/by_name/ for a name that does not exist (404)After: the same rejections identify themselves as bad requests, so the script stops on the first one and prints the actual problem
{"budget_duration": "not-a-duration"}{"type":"invalid_request_error","code":"400"}with the same message about the duration format"30d"and the key is createdteam_id, the duplicateuser_id, and the missing credential name behave the same way: one attempt,invalid_request_error, the message they needinternal_server_error, so the retry loop keeps working where retrying is the right thing to doRelevant issues
Linear ticket
Resolves LIT-6839
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Shared setup, identical for both legs: same config, same Postgres, same seeded rows, two uvicorn workers each, so the only difference between the two runs is the commit. Every
PORTbelow was a random free high port, one per leg.config.yaml:Boot, run once per leg from that leg's own checkout:
The
t6839dupteam, theu6839dupuser and thec6839dupcredential are created first so the duplicate-key probes have something to collide with. Thegrayswan-unreachableguardrail points at a port nothing listens on, which is how a real connection failure gets an exception carryingstatus_code=Noneinto this handler.Before (7a5b8bc)
Keys
curl -sS -X POST 'http://127.0.0.1:PORT/key/generate' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"budget_duration":"not-a-duration"}'curl -sS -X POST 'http://127.0.0.1:PORT/key/delete' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"keys":["sk-nonexistent-6839"]}'Teams
curl -sS -X POST 'http://127.0.0.1:PORT/team/new' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id":"t6839dup","team_alias":"t6839dup"}'curl -sS -X POST 'http://127.0.0.1:PORT/team/update' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id":"no-such-team-6839","team_alias":"x"}'curl -sS -X PATCH 'http://127.0.0.1:PORT/team/no-such-team-6839' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias":"x"}'Internal users
curl -sS -X POST 'http://127.0.0.1:PORT/user/new' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id":"u6839dup","user_email":"u6839dup@example.com"}'curl -sS -X GET 'http://127.0.0.1:PORT/user/info?user_id=no-such-user-6839' -H 'Authorization: Bearer sk-1234'Credentials
curl -sS -X GET 'http://127.0.0.1:PORT/credentials/by_name/does-not-exist-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/credentials/by_model/no-such-model-6839' -H 'Authorization: Bearer sk-1234'Guardrails
curl -sS -X POST 'http://127.0.0.1:PORT/guardrails/apply_guardrail' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"guardrail_name":"no-such-guardrail-6839","text":"hi"}'Batches
curl -sS -X GET 'http://127.0.0.1:PORT/v1/batches/batch_nonexistent6839' -H 'Authorization: Bearer sk-1234'curl -sS -X POST 'http://127.0.0.1:PORT/v1/batches/batch_nonexistent6839/cancel' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/openai/v1/batches/batch_nonexistent6839' -H 'Authorization: Bearer sk-1234'Unchanged: routes that raise ProxyException directly
curl -sS -X GET 'http://127.0.0.1:PORT/key/info?key=sk-nonexistent-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/customer/info?end_user_id=no-such-cust-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/team/info?team_id=no-such-team-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X POST 'http://127.0.0.1:PORT/user/update' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id":"u6839dup","budget_duration":"not-a-duration"}'Unchanged: a genuine 500
curl -sS -X POST 'http://127.0.0.1:PORT/credentials' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"credential_name":"c6839dup","credential_values":{"api_key":"x"},"credential_info":{"a":"b"}}'curl -sS -X GET 'http://127.0.0.1:PORT/global/spend/report?start_date=not-a-date&end_date=also-bad' -H 'Authorization: Bearer sk-1234'A guardrail whose upstream is unreachable
curl -sS -X POST 'http://127.0.0.1:PORT/guardrails/apply_guardrail' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"guardrail_name":"grayswan-unreachable","text":"hi"}'Liveness, a real OpenAI call through the proxy
curl -sS -X POST 'http://127.0.0.1:PORT/v1/chat/completions' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Reply with exactly: LIT-6839 QA leg live"}]}'After (6a9cb48)
Keys
curl -sS -X POST 'http://127.0.0.1:PORT/key/generate' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"budget_duration":"not-a-duration"}'curl -sS -X POST 'http://127.0.0.1:PORT/key/delete' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"keys":["sk-nonexistent-6839"]}'Teams
curl -sS -X POST 'http://127.0.0.1:PORT/team/new' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id":"t6839dup","team_alias":"t6839dup"}'curl -sS -X POST 'http://127.0.0.1:PORT/team/update' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id":"no-such-team-6839","team_alias":"x"}'curl -sS -X PATCH 'http://127.0.0.1:PORT/team/no-such-team-6839' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias":"x"}'Internal users
curl -sS -X POST 'http://127.0.0.1:PORT/user/new' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id":"u6839dup","user_email":"u6839dup@example.com"}'curl -sS -X GET 'http://127.0.0.1:PORT/user/info?user_id=no-such-user-6839' -H 'Authorization: Bearer sk-1234'Credentials
curl -sS -X GET 'http://127.0.0.1:PORT/credentials/by_name/does-not-exist-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/credentials/by_model/no-such-model-6839' -H 'Authorization: Bearer sk-1234'Guardrails
curl -sS -X POST 'http://127.0.0.1:PORT/guardrails/apply_guardrail' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"guardrail_name":"no-such-guardrail-6839","text":"hi"}'Batches
curl -sS -X GET 'http://127.0.0.1:PORT/v1/batches/batch_nonexistent6839' -H 'Authorization: Bearer sk-1234'curl -sS -X POST 'http://127.0.0.1:PORT/v1/batches/batch_nonexistent6839/cancel' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/openai/v1/batches/batch_nonexistent6839' -H 'Authorization: Bearer sk-1234'Unchanged: routes that raise ProxyException directly
curl -sS -X GET 'http://127.0.0.1:PORT/key/info?key=sk-nonexistent-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/customer/info?end_user_id=no-such-cust-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X GET 'http://127.0.0.1:PORT/team/info?team_id=no-such-team-6839' -H 'Authorization: Bearer sk-1234'curl -sS -X POST 'http://127.0.0.1:PORT/user/update' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id":"u6839dup","budget_duration":"not-a-duration"}'Unchanged: a genuine 500
curl -sS -X POST 'http://127.0.0.1:PORT/credentials' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"credential_name":"c6839dup","credential_values":{"api_key":"x"},"credential_info":{"a":"b"}}'curl -sS -X GET 'http://127.0.0.1:PORT/global/spend/report?start_date=not-a-date&end_date=also-bad' -H 'Authorization: Bearer sk-1234'A guardrail whose upstream is unreachable
curl -sS -X POST 'http://127.0.0.1:PORT/guardrails/apply_guardrail' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"guardrail_name":"grayswan-unreachable","text":"hi"}'Liveness, a real OpenAI call through the proxy
curl -sS -X POST 'http://127.0.0.1:PORT/v1/chat/completions' -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Reply with exactly: LIT-6839 QA leg live"}]}'Type
🐛 Bug Fix
Caveats (if any)
Medium
429reaching this handler now answersthrottling_errorLow
status_codeon an exception now falls back to 500status_codeauth_erroron 400s and 404s are left aloneFinal Attestation
Note
Medium Risk
Broad proxy error-path behavior change affects all routes using the shared handler; clients branching on
error.typewill see different strings on 4xx/429, though HTTP status and messages are unchanged.Overview
handle_exception_on_proxyno longer labels every wrapped exception asinternal_server_error. It now derivescodeviaerror_status_codeandtypeviaopenai_error_type, so client errors (400/404/409/422 →invalid_request_error, 401/403/429 → auth/permission/rate-limit) match the status returned while true 5xx responses stayinternal_server_error. Exceptions that already carry their owntype(e.g. LiteLLMRateLimitError→throttling_error) are left unchanged.Tests cover the handler mapping (including HTTP and
status_code-carrying exceptions), an integration check that a missing credential lookup returns 404 withinvalid_request_error, and updated expectations for 403 and custom status codes.Reviewed by Cursor Bugbot for commit 6a9cb48. Bugbot is set up for automated code reviews on this repo. Configure here.