diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index d3e8a74945..528857f5dd 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -1361,7 +1361,6 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): ) # Apply any masking that was applied by the guardrail - output_list = bedrock_response.get("output") if output_list: # If the guardrail returned modified content, use that @@ -1392,6 +1391,14 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): inputs["texts"] = masked_texts return inputs + except (HTTPException, GuardrailInterventionNormalStringError): + # Let guardrail blocking exceptions propagate as-is so the proxy + # can return the correct HTTP status (400) or handle the + # GuardrailInterventionNormalStringError for disable_exception_on_block mode. + # Without this, the generic except below wraps them into a plain + # Exception, losing the HTTP semantics and preventing the proxy + # from properly blocking the call. + raise except Exception as e: verbose_proxy_logger.error( "Bedrock Guardrail: Failed to apply guardrail: %s", str(e) diff --git a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py index 60d4f47973..4bea2255b5 100644 --- a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py +++ b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py @@ -4,13 +4,12 @@ Test the Bedrock guardrail apply_guardrail functionality import os import sys -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, patch import pytest sys.path.insert(0, os.path.abspath("../../../../..")) -from fastapi import HTTPException from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrail @@ -53,39 +52,42 @@ async def test_bedrock_apply_guardrail_success(): @pytest.mark.asyncio async def test_bedrock_apply_guardrail_blocked(): - """Test that Bedrock guardrail apply_guardrail method handles blocked content""" - # Create a BedrockGuardrail instance + """Test that apply_guardrail lets HTTPException propagate as-is for blocked content. + + Regression test for issue #20045: when disable_exception_on_block=False (default), + make_bedrock_api_request raises HTTPException for BLOCKED content. apply_guardrail + must NOT wrap it in a generic Exception, otherwise the proxy loses the HTTP 400 + status and fails to block the LLM call. + """ + from fastapi import HTTPException + guardrail = BedrockGuardrail( guardrail_name="test-bedrock-guard", guardrailIdentifier="test-guard-id", guardrailVersion="DRAFT", ) - # Mock the make_bedrock_api_request method to raise an exception for blocked content with patch.object( - guardrail, "make_bedrock_api_request", new_callable=AsyncMock + guardrail, "make_bedrock_api_request", new_callable=AsyncMock ) as mock_api_request: - # Mock the method to raise an HTTPException as it would for blocked content - from fastapi import HTTPException mock_api_request.side_effect = HTTPException( status_code=400, detail={ "error": "Violated guardrail policy", - "bedrock_guardrail_response": "", + "bedrock_guardrail_response": "Content blocked", }, ) - # Test the apply_guardrail method should raise an exception - with pytest.raises(Exception) as exc_info: + # HTTPException must propagate as-is (not wrapped in a generic Exception) + with pytest.raises(HTTPException) as exc_info: await guardrail.apply_guardrail( inputs={"texts": ["This is blocked content"]}, request_data={}, input_type="request", ) - # The apply_guardrail method wraps the original exception in a generic Exception - assert "Bedrock guardrail failed:" in str(exc_info.value) - assert "Violated guardrail policy" in str(exc_info.value) + assert exc_info.value.status_code == 400 + assert "Violated guardrail policy" in str(exc_info.value.detail) @pytest.mark.asyncio @@ -226,7 +228,10 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable with patch.object( guardrail, "make_bedrock_api_request", new_callable=AsyncMock ) as mock_api: - mock_api.return_value = {"action": "ALLOWED", "output": [{"text": "latest question"}]} + mock_api.return_value = { + "action": "ALLOWED", + "output": [{"text": "latest question"}], + } guardrailed_inputs = await guardrail.apply_guardrail( inputs={"texts": ["latest question"]}, @@ -262,6 +267,7 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable ) as mock_api: # Mock the method to raise an HTTPException as it would for blocked content from fastapi import HTTPException + mock_api.side_effect = HTTPException( status_code=400, detail={ @@ -270,7 +276,7 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable }, ) - with pytest.raises(Exception, match="policy") as exc_info: + with pytest.raises(HTTPException) as exc_info: await guardrail.apply_guardrail( inputs={"texts": ["blocked"]}, request_data=request_data, @@ -280,8 +286,9 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable assert mock_api.called _, kwargs = mock_api.call_args assert kwargs["messages"] == [request_messages[-1]] - # The apply_guardrail method wraps the original exception in a generic Exception - assert "Bedrock guardrail failed:" in str(exc_info.value) + # HTTPException must propagate as-is (not wrapped) + assert exc_info.value.status_code == 400 + assert "Violated guardrail policy" in str(exc_info.value.detail) def test_bedrock_guardrail_filters_latest_user_message_when_enabled(): @@ -312,3 +319,37 @@ def test_bedrock_guardrail_filters_latest_user_message_when_enabled(): target_indices=filter_result.target_indices, ) assert masked_messages[3]["content"] == "[MASKED]" + + +@pytest.mark.asyncio +async def test_bedrock_apply_guardrail_blocked_with_disable_exception_on_block(): + """ + Regression test for issue #20045: when disable_exception_on_block=True, + make_bedrock_api_request raises GuardrailInterventionNormalStringError. + apply_guardrail must let it propagate as-is so the proxy can handle it + properly instead of wrapping it in a generic Exception. + """ + from litellm.exceptions import GuardrailInterventionNormalStringError + + guardrail = BedrockGuardrail( + guardrail_name="test-bedrock-guard", + guardrailIdentifier="test-guard-id", + guardrailVersion="DRAFT", + disable_exception_on_block=True, + ) + + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api: + mock_api.side_effect = GuardrailInterventionNormalStringError( + message="Sorry, your question in its current format is unable to be answered." + ) + + with pytest.raises(GuardrailInterventionNormalStringError) as exc_info: + await guardrail.apply_guardrail( + inputs={"texts": ["harmful prompt content"]}, + request_data={}, + input_type="request", + ) + + assert "unable to be answered" in str(exc_info.value.message)