litellm - ✅(Solved) Fix GuardrailRaisedException returns HTTP 500 instead of 400 for blocked requests [4 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#24348Fetched 2026-04-08 01:13:11
View on GitHub
Comments
1
Participants
2
Timeline
7
Reactions
0
Author
Timeline (top)
cross-referenced ×4closed ×1commented ×1referenced ×1

Error Message

code=getattr(e, "status_code", 500), # no status_code on GuardrailRaisedException → 500

Root Cause

GuardrailRaisedException in litellm/exceptions.py has no status_code attribute. When it bubbles up to _handle_llm_api_exception() in litellm/proxy/common_request_processing.py, the fallback logic defaults to 500:

code=getattr(e, "status_code", 500),  # no status_code on GuardrailRaisedException → 500

Fix Action

Fix

PR: #24347 — adds status_code=400 to GuardrailRaisedException.

PR fix notes

PR #24473: fix(exceptions): return 400 for GuardrailRaisedException and BlockedPiiEntityError

Description (problem / solution / changelog)

Closes #24348

Summary

  • GuardrailRaisedException and BlockedPiiEntityError both lacked a status_code attribute, so the generic exception handler (getattr(e, "status_code", 500)) defaulted to HTTP 500.
  • Added status_code=400 to both classes. Guardrail blocks are client-triggered, not server errors.
  • This also addresses the gap noted in #24347's review — BlockedPiiEntityError (raised by the Presidio guardrail) had the same problem.

Changes

  • litellm/exceptions.py: Add status_code: int = 400 param and self.status_code to both GuardrailRaisedException and BlockedPiiEntityError
  • tests/test_litellm/test_guardrail_exception_status_codes.py: New test file covering default status code, custom status code, and the getattr fallback pattern for both exceptions
  • tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py: Added status_code == 400 assertion to existing blocked-action test

Test plan

  • 6 new unit tests pass (pytest tests/test_litellm/test_guardrail_exception_status_codes.py)
  • Existing blocked-action test updated with status_code assertion

Changed files

  • enterprise/litellm_enterprise/enterprise_callbacks/pagerduty/pagerduty.py (modified, +1/-0)
  • litellm/exceptions.py (modified, +5/-1)
  • litellm/llms/a2a/chat/guardrail_translation/handler.py (modified, +1/-1)
  • litellm/llms/anthropic/chat/guardrail_translation/handler.py (modified, +1/-1)
  • litellm/llms/openai/chat/guardrail_translation/handler.py (modified, +3/-3)
  • litellm/proxy/_experimental/mcp_server/rest_endpoints.py (modified, +2/-2)
  • litellm/proxy/management_helpers/audit_logs.py (modified, +9/-5)
  • litellm/proxy/spend_tracking/spend_tracking_utils.py (modified, +1/-0)
  • tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py (modified, +1/-0)
  • tests/test_litellm/test_guardrail_exception_status_codes.py (added, +58/-0)

PR #24507: fix(guardrails): return HTTP 400 for blocked requests, not 500

Description (problem / solution / changelog)

Summary

  • Add status_code = 400 to GuardrailRaisedException so intentional guardrail blocks return HTTP 400 instead of 500
  • Recognize GuardrailRaisedException in _is_guardrail_intervention() so blocks are logged as guardrail_intervened (not guardrail_failed_to_respond)
  • Add status_code assertion to existing generic guardrail API test

Problem

GuardrailRaisedException has no status_code attribute. When it reaches the catch-all in _handle_llm_api_exception():

code=getattr(e, "status_code", 500)  # no status_code → defaults to 500

This makes intentional guardrail blocks (content policy violations) return HTTP 500, indistinguishable from actual server errors. It also causes _is_guardrail_intervention() to return False, so downstream loggers (Langfuse, DataDog, etc.) record blocks as guardrail_failed_to_respond instead of guardrail_intervened.

Test plan

  • Existing test_action_blocked_raises_exception updated to assert status_code == 400
  • All 32 generic guardrail API tests pass

Fixes #24348

🤖 Generated with Claude Code

Changed files

  • enterprise/litellm_enterprise/enterprise_callbacks/pagerduty/pagerduty.py (modified, +1/-0)
  • litellm/exceptions.py (modified, +1/-0)
  • litellm/integrations/custom_guardrail.py (modified, +4/-0)
  • litellm/llms/openai/chat/guardrail_translation/handler.py (modified, +3/-3)
  • litellm/proxy/management_helpers/audit_logs.py (modified, +9/-5)
  • litellm/proxy/spend_tracking/spend_tracking_utils.py (modified, +1/-0)
  • ruff.toml (modified, +2/-0)
  • tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py (modified, +1/-0)

PR #24693: fix(guardrails): return HTTP 400 instead of 500 for Model Armor streaming blocks

Description (problem / solution / changelog)

When Model Armor blocks a streaming response, it correctly raises HTTPException(status_code=400) but create_response() catches it with a bare except Exception and hardcodes a 500 response, discarding the original status code.

Fix create_response() to preserve status_code from HTTPException instead of hardcoding 500. Also update Model Armor's streaming hook to yield an SSE error event instead of raising (matching the Prisma Airs pattern), and fix make_model_armor_request() to return 400 for upstream API failures instead of passing through the upstream status code.

Relevant issues

Fixes #24348

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🐛 Bug Fix

Changes

litellm/proxy/common_request_processing.py

  • create_response(): Replace hardcoded HTTP_500_INTERNAL_SERVER_ERROR in the except Exception handler with getattr(e, "status_code", 500). This preserves the original status code from HTTPException (e.g., 400 from guardrail blocks) while still defaulting to 500 for unexpected exceptions. Also extracts e.detail for the error message instead of the generic "Error processing stream start".

litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py

  • Streaming hook (async_post_call_streaming_iterator_hook): Replace except HTTPException: raise with a yield-error pattern that emits the error as an SSE data event. This follows the existing Prisma Airs convention and allows create_response() to detect the error via _parse_event_data_for_error() and return a proper JSONResponse with the correct status code.
  • make_model_armor_request(): Change status_code=response.status_code to status_code=400 so upstream Model Armor API failures (e.g., 500, 503) are reported as 400 to the client instead of leaking the upstream status code. The original upstream code is preserved in the detail message for debugging.

Tests added

  • test_create_streaming_response_generator_raises_http_exception — verifies create_response() preserves HTTPException status code (400) instead of hardcoding 500
  • test_model_armor_streaming_block_yields_sse_error — verifies streaming content block yields SSE error event with code 400 instead of raising
  • test_model_armor_api_failure_returns_400 — verifies upstream API failures return 400, not the passthrough status code
  • Updated test_model_armor_api_error_handling assertion to match new 400 behavior

Changed files

  • litellm/proxy/common_request_processing.py (modified, +8/-3)
  • litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py (modified, +19/-4)
  • tests/test_litellm/proxy/guardrails/guardrail_hooks/test_model_armor.py (modified, +124/-1)
  • tests/test_litellm/proxy/test_common_request_processing.py (modified, +28/-1)

PR #24961: fix: add status_code=400 to guardrail exception classes

Description (problem / solution / changelog)

Summary

GuardrailRaisedException and BlockedPiiEntityError lack a status_code attribute. When these exceptions are raised, the proxy exception handler at common_request_processing.py:1629 falls back to HTTP 500:

code=getattr(e, "status_code", 500),  # no status_code → 500

Guardrail blocks are client-side validation errors and should return HTTP 400, not 500.

Why this matters: Clients typically retry on 5xx but not 4xx. Returning 500 for guardrail blocks can cause retry loops that repeatedly attempt to bypass PII/content protections.

Changes

  • Add status_code: int = 400 parameter to GuardrailRaisedException.__init__()
  • Add status_code: int = 400 parameter to BlockedPiiEntityError.__init__()
  • Both store self.status_code so getattr(e, "status_code", 500) resolves to 400

Related

  • #24348 reports the same symptom for GuardrailRaisedException specifically
  • This PR also fixes the sibling BlockedPiiEntityError class which has the identical issue

Test plan

  • Verify GuardrailRaisedException().status_code == 400
  • Verify BlockedPiiEntityError("SSN").status_code == 400
  • Existing callers unaffected (parameter is keyword-only with default)

Changed files

  • litellm/exceptions.py (modified, +4/-0)

Code Example

HTTP/1.1 400 Bad Request
{"error": {"message": "Content blocked by guardrail", "type": "None", "param": "None", "code": "400"}}

---

HTTP/1.1 500 Internal Server Error
{"error": {"message": "pci detected", "type": "None", "param": "None", "code": "500"}}

---

code=getattr(e, "status_code", 500),  # no status_code on GuardrailRaisedException500

---

curl http://localhost:4000/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer <key>" -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "My credit card is 5555-5555-5555-4444"}]}'
RAW_BUFFERClick to expand / collapse

Bug Description

When a guardrail blocks a request (e.g. PCI data detected, prompt injection, etc.), the LiteLLM proxy returns HTTP 500 instead of the documented HTTP 400.

Expected Behavior

Per the Generic Guardrail API docs, blocked requests should return HTTP 400:

HTTP/1.1 400 Bad Request
{"error": {"message": "Content blocked by guardrail", "type": "None", "param": "None", "code": "400"}}

Actual Behavior

HTTP/1.1 500 Internal Server Error
{"error": {"message": "pci detected", "type": "None", "param": "None", "code": "500"}}

Root Cause

GuardrailRaisedException in litellm/exceptions.py has no status_code attribute. When it bubbles up to _handle_llm_api_exception() in litellm/proxy/common_request_processing.py, the fallback logic defaults to 500:

code=getattr(e, "status_code", 500),  # no status_code on GuardrailRaisedException → 500

Reproduction

  1. Configure a guardrail (any provider using the Generic Guardrail API)
  2. Send a request with content that triggers a block (e.g. credit card number)
  3. Observe HTTP 500 in the response
curl http://localhost:4000/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer <key>" -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "My credit card is 5555-5555-5555-4444"}]}'

Fix

PR: #24347 — adds status_code=400 to GuardrailRaisedException.

extent analysis

Fix Plan

To fix the issue, we need to add a status_code attribute to the GuardrailRaisedException class. Here are the steps:

  • Update the GuardrailRaisedException class in litellm/exceptions.py to include a status_code attribute:
class GuardrailRaisedException(Exception):
    def __init__(self, message, status_code=400):
        self.message = message
        self.status_code = status_code
        super().__init__(message)
  • Verify that the _handle_llm_api_exception() function in litellm/proxy/common_request_processing.py correctly handles the updated GuardrailRaisedException class.

Verification

To verify the fix, you can use the following steps:

  • Configure a guardrail using the Generic Guardrail API
  • Send a request with content that triggers a block (e.g. credit card number) using the curl command:
curl http://localhost:4000/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer <key>" -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "My credit card is 5555-5555-5555-4444"}]}'
  • Check the response status code, which should now be HTTP 400 instead of HTTP 500.

Extra Tips

  • Make sure to update the documentation to reflect the correct status code returned by the API.
  • Consider adding additional logging or monitoring to detect and handle similar issues in the future.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING