fix(guardrails): key the streamed block decision on status, not detail shape

InvokeGuardrailChecks details a Mapping on its 500 for an unparseable response,
so a detail-shape test read that outage as a policy block and framed it as a 200
guardrail_error. Both block sites raise 400, so gate on the status too.
This commit is contained in:
Yucheng Zhu 2026-08-12 20:11:12 -07:00
parent ab9bb303cf
commit f0da259f66
2 changed files with 21 additions and 5 deletions

View file

@ -2626,10 +2626,10 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
)
except HTTPException as block_exc:
block_detail: Final = block_exc.detail
# Only a policy block carries a dict detail; every API/transport failure details a
# plain string (see _is_input_too_large_error). Re-raising those keeps their real
# status instead of reporting an outage as a guardrail decision
if not raw_sse or not isinstance(block_detail, Mapping):
# A policy block is the only 400 carrying a structured detail; a service failure
# either details a plain string or reports a non-400 status, and re-raising those
# keeps their real status instead of reporting an outage as a guardrail decision
if not raw_sse or block_exc.status_code != 400 or not isinstance(block_detail, Mapping):
raise
# past the keepalive ping the headers are already flushed, so a raise cannot reach
# the client; the block travels as a frame

View file

@ -2958,7 +2958,7 @@ async def test_streaming_hook_block_stream_keeps_upstream_identity():
async def test_streaming_hook_reraises_guardrail_service_failures():
"""A Bedrock outage must keep its status, not be reported to the caller as a guardrail decision.
Only a policy block details a Mapping; every API/transport failure details a plain string.
A policy block is the only 400 detailing a Mapping.
"""
guardrail = _sse_guardrail()
@ -2972,6 +2972,22 @@ async def test_streaming_hook_reraises_guardrail_service_failures():
assert exc.value.status_code == 500
@pytest.mark.asyncio
async def test_streaming_hook_reraises_a_service_failure_that_details_a_mapping():
"""InvokeGuardrailChecks details a Mapping on its 500, so detail shape alone cannot mean "block"."""
guardrail = _sse_guardrail()
with patch.object(guardrail, "make_bedrock_api_request", new_callable=AsyncMock) as mock_api:
mock_api.side_effect = HTTPException(
status_code=500,
detail={"error": "Bedrock InvokeGuardrailChecks returned an unexpected response shape"},
)
with pytest.raises(HTTPException) as exc:
await _drain_streaming_hook(guardrail)
assert exc.value.status_code == 500
@pytest.mark.asyncio
async def test_streaming_block_error_frame_message_is_a_string():
"""AnthropicErrorDetail.message is typed str, built by the proxy's own detail serializer."""