fix(guardrails): return HTTP 400 for litellm content filter blocks - #28418
Conversation
Align litellm_content_filter hard rejects with the standard guardrail block status code so clients receive 400 instead of 403. Co-authored-by: Cursor <cursoragent@cursor.com>
Greptile SummaryThis PR standardises guardrail block status codes to HTTP 400 and fixes custom-code guardrail pre-call passthrough semantics. The 403 → 400 change is an intentional, acknowledged breaking change that will be captured in release notes.
Confidence Score: 5/5Safe to merge. The 403 → 400 change is intentional and acknowledged; the custom-code guardrail passthrough fix is minimal and well-tested. The status-code change is a deliberate, documented breaking change with comprehensive test coverage across all affected paths. The custom-code guardrail fix correctly prevents pre-call passthrough exceptions from being swallowed as 500 errors, and the new pipeline tests validate end-to-end block behavior. No logic errors, no unhandled edge cases. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py | Five HTTPException status codes changed from 403 to 400 across all block paths; intentional breaking change, consistent with other guardrails and the existing _is_guardrail_intervention check. |
| litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py | Adds except ModifyResponseException: raise to prevent pre-call passthrough blocks from being wrapped as 500 execution errors; the fix is minimal and correct. |
| tests/test_litellm/proxy/guardrails/test_custom_code_security.py | Two new tests covering pre-call passthrough (ModifyResponseException) and post-call HTTP 400 block for custom code guardrails; tests correctly differentiate input_type="request" vs "response". |
| tests/test_litellm/proxy/policy_engine/test_pipeline_executor.py | Two new pipeline integration tests added: one for a generic PassthroughBlockGuardrail and one for CustomCodeGuardrail; both assert terminal_action=="block" and outcome=="fail" for on_fail: block steps. |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py | Bulk test update: 15 status_code assertions changed from 403 to 400, matching the production change; no test logic weakened. |
| litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/guardrail_benchmarks/test_eval.py | Benchmark harness updated to check 400 instead of 403; also updates the LLM-judge stub to raise 400; consistent with the production change. |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_competitor_intent.py | Single assertion updated from 403 to 400; no logic change. |
Reviews (2): Last reviewed commit: "fix(guardrails): preserve custom code pa..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
@greptile "Breaking change: 403 → 400 without a migration path" this is expected. We will mention the breaking changes in our release notes. Please update score based on this. |
|
Acknowledged — if the breaking change is documented in the release notes, that concern is resolved. The status code change is well-motivated (aligning with how LiteLLM's guardrail telemetry distinguishes Updated score: 4/5 — the change is correct and purposeful. The remaining point is for the lack of a feature flag option (per the backwards-compatibility rule), but given this is an intentional, documented breaking change rather than an accidental regression, that's a reasonable trade-off. Tip: You can give me more context with .greptile/rules.md so I get this right next time. |
…ontent_filter_block_response_to_400
…ontent_filter_block_response_to_400
Pre-call custom code guardrail blocks now raise HTTPException(400) instead of using the passthrough ModifyResponseException path that returned a synthetic 200 response. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep standalone custom code guardrail blocks on the passthrough contract while covering policy pipeline block handling for passthrough-style guardrail interventions. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@greptile review again holistically. note that "Breaking change: 403 → 400 without a migration path" this is expected. We will mention the breaking changes in our release notes. Please update score based on this. |
4c3efe9
into
litellm_internal_staging
…erriAI#28418) * fix(guardrails): return HTTP 400 for litellm content filter blocks Align litellm_content_filter hard rejects with the standard guardrail block status code so clients receive 400 instead of 403. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(guardrails): return HTTP 400 for custom code guardrail blocks Pre-call custom code guardrail blocks now raise HTTPException(400) instead of using the passthrough ModifyResponseException path that returned a synthetic 200 response. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(guardrails): preserve custom code passthrough blocks Keep standalone custom code guardrail blocks on the passthrough contract while covering policy pipeline block handling for passthrough-style guardrail interventions. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Resolves LIT-3253
Description
This PR updates guardrail block handling so policy-driven blocks use HTTP 400 consistently, while preserving the existing passthrough contract for standalone custom code guardrails.
Cause
Some guardrail block paths returned inconsistent client-facing status codes. Content filter blocks were using 403, while policy-builder block behavior should represent a bad/blocked request as 400. Custom code guardrails also needed to continue supporting their standalone passthrough behavior while still being treated as a block when used in policy pipelines.
Fix


Changed content filter block responses from 403 to 400.
Preserved standalone custom code guardrail pre-call block(...) behavior via passthrough synthetic response.
Kept post-call custom code guardrail blocks as HTTP 400.
Ensured passthrough-style guardrail interventions can be treated as pipeline failures, so policy builder on_fail: block returns a true block response.
Added tests for custom code passthrough behavior and policy pipeline block handling.
Tests
uv run pytest tests/test_litellm/proxy/policy_engine/test_pipeline_executor.py tests/test_litellm/proxy/guardrails/test_custom_code_security.py -v
Before:
Api trigger for block was raising api failure.
Content-filter guardrails
Custom guardrails:
After:


Content-filter guardrails
Api trigger for block raises block as expected.
Custom guardrails: