From 90cd6538d475fc7b3a8197fa19dc523ed244a5c9 Mon Sep 17 00:00:00 2001 From: Shivam Rawat <161387515+shivamrawat1@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:34:40 -0800 Subject: [PATCH] added functionality to propagate bedrock guardrail errors down to litellm (#20395) --- .../guardrail_hooks/bedrock_guardrails.py | 60 ++++++++++++++++++- .../test_bedrock_apply_guardrail.py | 8 +-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index 528857f5dd1..f8fba5f5984 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -461,12 +461,37 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): data=prepared_request.body, # type: ignore headers=prepared_request.headers, # type: ignore ) + except HTTPException: + # Propagate HTTPException (e.g. from non-200 path) as-is + raise except Exception as e: + # If this is an HTTP error with a response body (e.g. httpx.HTTPStatusError), + # extract the AWS error message and propagate it + response = getattr(e, "response", None) + if isinstance(response, httpx.Response): + try: + status_code, detail_message = ( + self._parse_bedrock_guardrail_error_response(response) + ) + self.add_standard_logging_guardrail_information_to_request_data( + guardrail_provider=self.guardrail_provider, + guardrail_json_response={"error": detail_message}, + request_data=request_data or {}, + guardrail_status="guardrail_failed_to_respond", + start_time=start_time.timestamp(), + end_time=datetime.now().timestamp(), + duration=(datetime.now() - start_time).total_seconds(), + event_type=event_type, + ) + raise HTTPException( + status_code=status_code, detail=detail_message + ) from e + except HTTPException: + raise # Endpoint down, timeout, or other HTTP/network errors verbose_proxy_logger.error( "Bedrock AI: failed to make guardrail request: %s", str(e) ) - # Add guardrail information with failure status self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, guardrail_json_response={"error": str(e)}, @@ -477,7 +502,6 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): duration=(datetime.now() - start_time).total_seconds(), event_type=event_type, ) - # Re-raise the exception to maintain existing behavior raise ######################################################### @@ -509,11 +533,15 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): bedrock_guardrail_response ) else: + status_code, detail_message = self._parse_bedrock_guardrail_error_response( + httpx_response + ) verbose_proxy_logger.error( "Bedrock AI: error in response. Status code: %s, response: %s", httpx_response.status_code, httpx_response.text, ) + raise HTTPException(status_code=status_code, detail=detail_message) return bedrock_guardrail_response @@ -579,6 +607,34 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): return "success" return "guardrail_failed_to_respond" + def _parse_bedrock_guardrail_error_response( + self, response: httpx.Response + ) -> Tuple[int, str]: + """ + Parse AWS Bedrock guardrail error response body to extract status code and message. + + AWS may return shapes like {"message": "..."} or {"error": {"message": "..."}}. + Returns (status_code, message) for use in HTTPException. + """ + status_code = response.status_code + message = "Bedrock guardrail request failed" + try: + body = response.json() + except Exception: + text = getattr(response, "text", None) or "" + if isinstance(text, str) and text.strip(): + return (status_code, text.strip()) + return (status_code, message) + if isinstance(body, dict): + if isinstance(body.get("message"), str): + return (status_code, body["message"]) + err = body.get("error") + if isinstance(err, dict) and isinstance(err.get("message"), str): + return (status_code, err["message"]) + if isinstance(err, str): + return (status_code, err) + return (status_code, message) + def _get_http_exception_for_blocked_guardrail( self, response: BedrockGuardrailResponse ) -> Union[HTTPException, GuardrailInterventionNormalStringError]: 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 4bea2255b56..dff444168c2 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 @@ -78,7 +78,7 @@ async def test_bedrock_apply_guardrail_blocked(): }, ) - # HTTPException must propagate as-is (not wrapped in a generic Exception) + # Test the apply_guardrail method propagates HTTPException (AWS error) to the client with pytest.raises(HTTPException) as exc_info: await guardrail.apply_guardrail( inputs={"texts": ["This is blocked content"]}, @@ -276,7 +276,7 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable }, ) - with pytest.raises(HTTPException) as exc_info: + with pytest.raises(HTTPException, match="policy") as exc_info: await guardrail.apply_guardrail( inputs={"texts": ["blocked"]}, request_data=request_data, @@ -286,9 +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]] - # HTTPException must propagate as-is (not wrapped) + # HTTPException from guardrail is propagated so the client gets the AWS message assert exc_info.value.status_code == 400 - assert "Violated guardrail policy" in str(exc_info.value.detail) + assert "policy" in str(exc_info.value.detail) def test_bedrock_guardrail_filters_latest_user_message_when_enabled():