{"record":{"id":"3a7dabc21a174dfc","repo":"BerriAI/litellm","slug":"blocked-by-custom-code-guardrail","errorCode":null,"errorMessage":"Blocked by custom code guardrail","messagePattern":"Blocked by custom code guardrail","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py","lineNumber":342,"sourceCode":"            reason: Final = result.get(\"reason\", \"Blocked by custom code guardrail\")\n            detection_info: Final = result.get(\"detection_info\", {})\n\n            verbose_proxy_logger.info(\n                \"Custom code guardrail '%s': Blocking %s - %s\", self.guardrail_name, input_type, reason\n            )\n\n            is_output: Final = input_type == \"response\"\n\n            # For pre-call, raise passthrough exception to return synthetic response\n            if not is_output:\n                self.raise_passthrough_exception(\n                    violation_message=reason,\n                    request_data=request_data,\n                    detection_info=detection_info,\n                )\n\n            # For post-call, raise HTTP exception\n            raise HTTPException(\n                status_code=400,\n                detail={\n                    \"error\": reason,\n                    \"guardrail\": self.guardrail_name,\n                    \"detection_info\": detection_info,\n                },\n            )\n\n        elif action == \"modify\":\n            verbose_proxy_logger.debug(\"Custom code guardrail '%s': Modifying %s\", self.guardrail_name, input_type)\n\n            # Apply modifications\n            modified_inputs: Final = dict(inputs)\n\n            if \"texts\" in result and result[\"texts\"] is not None:\n                modified_inputs[\"texts\"] = result[\"texts\"]\n\n            if \"images\" in result and result[\"images\"] is not None:","sourceCodeStart":324,"sourceCodeEnd":360,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py#L324-L360","documentation":"HTTP 400 raised by the custom-code guardrail when the user function's verdict is a block on a response (post-call): detail carries the block reason ('Blocked by custom code guardrail' unless a custom reason was given), the guardrail name, and detection_info. The same verdict pre-call instead raises a passthrough exception that returns a synthetic response to the client, so this 400 shape is specifically the post-call path.","triggerScenarios":"apply_guardrail returns block(...) while input_type == 'response' — the model output matched the custom rule (e.g. PII leaked in a completion, or a response-rejection phrase template); the post-call hook converts the verdict to a 400 for the caller.","commonSituations":"Output filters for refusal phrases or PII in completions (including the shipped RESPONSE_REJECTION_GUARDRAIL_CODE); clients see a 400 whose detail.guardrail names the custom guardrail and mistake it for a bug rather than a verdict.","solutions":["Treat as expected behavior: surface detail.error and detail.detection_info to the caller; do not retry — the block is deterministic for the same output","Tune the custom rule if it over-blocks (widen allow patterns, narrow the regex)","Move the check to a pre-call hook if you want a synthetic response instead of a post-call 400","Use the block reason string (block('reason')) so clients get an actionable message instead of the generic default"],"exampleFix":"# custom_code: before — default generic reason\nreturn block()\n\n# custom_code: after — actionable reason\nreturn block('Response contained a customer account number')\n\n# caller: handle the 400 explicitly\ntry:\n    out = await client.chat.completions.create(**params)\nexcept Exception as e:\n    body = getattr(getattr(e, 'response', None), 'json', lambda: {})() or {}\n    err = body.get('error', {})\n    if err and body.get('guardrail') == 'my-custom-guardrail':\n        return blocked_to_user(err, body.get('detection_info'))\n    raise","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    out = await client.chat.completions.create(**params)\nexcept Exception as e:\n    body = getattr(getattr(e, 'response', None), 'json', lambda: {})() or {}\n    err = body.get('error')\n    if isinstance(err, str) and body.get('guardrail'):\n        return blocked_to_user(err, body.get('detection_info'))  # verdict, not a bug; no retry\n    raise","preventionTips":["Give block(...) a specific reason string so downstream handlers can branch on it","Remember the pre-call path returns a synthetic response while post-call raises 400 - pick the hook that matches your UX","Do not put guardrail 400s into retry policies; they are deterministic for identical outputs","Return detection_info to internal callers for audit trails, but sanitize it before showing end users"],"tags":["guardrails","custom-code","http-400","policy-violation","blocked-request","output-filtering"],"backgroundTag":"guardrail-content-blocked","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}