Azure OpenAI content filter errors: handle blocked prompts and cut-off answers
Azure OpenAI's content filter stops a request in two different ways: a 400 for a blocked prompt, and a 200 whose answer was cut. Handle both in code, read the annotations, know what the default guardrail does, and tune it in Foundry instead of fighting it.
AI & agentsFix a problem6 min read
Every model in Microsoft Foundry runs behind a content filter, and Azure OpenAI calls meet it in two shapes that look nothing alike: an error for a prompt that was blocked, and a normal-looking success whose answer was cut short. Apps that handle only one of them show users blank or truncated replies with no explanation, and sometimes retry a blocked prompt in a loop. This guide covers both shapes, the annotations that explain them, what the default filter does, and how to change it where that's the right call.
The two shapes a filter takes
The prompt was blocked. The call fails with HTTP 400 and the error code content_filter. This is the same for chat completions and for the Responses API:
{
"error": {
"code": "content_filter",
"message": "The response was filtered due to the prompt triggering content management policy."
}
}
The answer was filtered. The call succeeds with HTTP 200, because the prompt was fine, but what the model produced tripped the filter:
- Chat completions, not streaming: the choice has
finish_reason: "content_filter"and, as a rule, no content. On long answers a partial result can come back with that reason. - Chat completions, streaming: chunks arrive normally until filtered content is detected; the last chunk for that choice carries
finish_reason: "content_filter". - The Responses API: the response object has a top-level
content_filtersarray, one entry per result, withblocked,source_type(promptorcompletion), the category results and the character offsets. It's a Foundry extension, so the SDKs don't give it a typed property: read it as an extra field.
Not every 400 is the filter. A prompt over the context length, an unsupported parameter or a malformed body also return 400, so check the code before you decide what to tell the user.
Handle both in code
import openai
try:
response = client.responses.create(model="gpt-5-mini", input=user_text)
except openai.BadRequestError as error:
body = getattr(error, "body", None) or {}
if (body.get("error") or {}).get("code") == "content_filter":
# The prompt was blocked. Say so; don't resend it unchanged.
show("That request can't be answered here. Please rephrase it.")
else:
raise
else:
filters = getattr(response, "model_extra", {}).get("content_filters") or []
if any(result.get("blocked") for result in filters):
# The answer was cut. Don't present it as complete.
show_partial(response.output_text)
else:
show(response.output_text)
Three rules behind that code:
- Don't retry a blocked prompt as it is. Retries can't change the verdict and are billed each time. Rephrasing, by the user or your code, is the only way forward.
- Don't present a filtered answer as whole. Tell the user the reply was cut, and offer to rephrase.
- Log the category, not the text. The annotations (below) say which category fired and at what severity; that's what you need to tune the filter later, and it keeps user content out of your logs.
It costs money either way. Microsoft bills for processing, not for success: a 400 from the filter is charged for the prompt, and a 200 with content_filter is charged for the prompt and the tokens generated before the cut. Only requests the service never processed, such as a 401 or a 429, are free.
Read the annotations
With annotations on, every response carries the filter's verdict for the prompt and the completion: for hate, sexual, violence and self-harm, the severity (safe, low, medium or high) and whether it was filtered; for prompt shields and protected material, whether it was detected and filtered; for protected code, a citation and the licence of the public repository it matched. Chat completions return them as prompt_filter_results and content_filter_results; the Responses API puts them in content_filters. If your app shows code, show the citation too.
What the default guardrail does
A Foundry guardrail is a named set of controls, and every model deployment starts on Microsoft's default one, Microsoft.DefaultV2:
| Risk | Checked on | Default |
|---|---|---|
| Hate and fairness, Sexual, Violence, Self-harm | Prompts and completions | Blocks at Medium severity and above |
| User prompt attacks (jailbreak), through Prompt Shields | Prompts | On |
| Protected material, text and code | Completions | On |
| Indirect attacks (instructions hidden in documents you pass in) | Prompts | Off until you turn it on, and only on text you tag as documents |
Severity has four levels. Safe is always annotated and never blocked. A threshold of Low blocks low and above, the strictest useful setting; Medium is the default; High blocks only the most severe content. Off exists but is only available to customers Microsoft has approved for modified guardrails, by application. The text classifiers are trained and tested on English, German, Japanese, Spanish, French, Italian, Portuguese and Chinese; other languages work, with less certainty, so test in yours.
Content classified as Safe that still looks like it might be filtered usually isn't: medical, historical or educational uses of difficult words are meant to pass. When a legitimate case is blocked anyway, the fix is in the guardrail, not in longer prompts.
Tune the guardrail, not the prompt
Guardrails live in the new Foundry portal under Build > Guardrails; you need the Foundry Account Owner role or higher.
- Select Create Guardrail. Step 1 lists the default controls; pick a risk, the intervention points (user input and output for models; agents add tool call and tool response) and the action: Annotate or Annotate and block.
- To loosen a default category you can't delete (hate, sexual, violence and self-harm on input and output), add the same risk again with the new threshold and confirm the override.
- Step 2 assigns the guardrail to models and agents. An agent's own guardrail replaces its model's entirely, so an agent with none assigned inherits the model's, and one with its own ignores the model's settings.
- Name it and Create. It applies at once. Select the guardrail and Try in Playground to see what "Annotate and block" does before production traffic meets it.
For one-off calls, a request can name a different guardrail with the x-policy-id header; it overrides the deployment's for that call, except for image input. A name that doesn't exist returns InvalidContentFilterPolicy.
Two costs to remember. Each intervention point adds roughly 50 to 100 ms to a call, so keep agents on the controls they need. And turning the filter down is a decision about what reaches your users: Microsoft's advice is to start strict, relax one category at a time, and measure again after every change.
Sources
- Use the Azure OpenAI Responses API: handle guardrails and content filtering, Microsoft Learn
- Content filtering for Microsoft Foundry Models: scenarios, Microsoft Learn
- Default guardrail policies for Microsoft Foundry, Microsoft Learn
- Guardrails and controls overview, Microsoft Learn
- How to configure guardrails and controls, Microsoft Learn
- Harm categories and severity levels, Microsoft Learn
- Prompt Shields in Microsoft Foundry, Microsoft Learn
- Azure OpenAI frequently asked questions, Microsoft Learn (billing for filtered requests)
Did this fix it?
Comments
No comments yet. Share what worked for you.
Sign in to comment. Comments show under a display name you choose, never your email.