finish_reason: content_filter (200 OK)
An OpenAI Chat Completions response can return HTTP 200 OK while finish_reason is content_filter, meaning the model’s output was withheld or truncated by the safety system mid-generation. The request “succeeded.” The user got a broken or empty answer. Your uptime monitor, which watches status codes, saw a green 200 and stayed silent. 200 OK is a lie here: the transport layer reports success while the application layer failed.
200 OK with finish_reason: content_filter and empty or truncated content. Status-code monitoring sees a healthy 200 and never fires. If you only log non-2xx responses, this failure is invisible. Log finish_reason on every call, alert on the rate of content_filter, and treat it as a real failure path in code.
What it looks like
The response is well-formed. The status is 200. The content is empty or cut off, and finish_reason is content_filter instead of the normal stop:
{
"choices": [{
"message": { "role": "assistant", "content": "" },
"finish_reason": "content_filter"
}],
"usage": { "prompt_tokens": 41, "completion_tokens": 0, "total_tokens": 41 }
}
Nothing in the HTTP transport signals a problem. The refusal is a field in the body, not a status code and not an exception your client will throw.
Why it happens
OpenAI’s moderation runs during generation, not only as a pre-flight check. When the output being generated trips a policy threshold, generation halts and finish_reason is set to content_filter. Because the request itself was accepted and processed successfully, the HTTP status stays 200. The model did its job up to the point the filter intervened; the API is reporting “I handled your request,” not “your request failed.”
Why your monitoring can’t see it
Uptime checks, load-balancer health probes, and status-code dashboards all key on HTTP status. A 200 is “healthy,” full stop. Catching this requires parsing the response body for finish_reason: content_filter on every single call, which almost no team logs, let alone alerts on. The failure is invisible to the exact tools teams trust to tell them something broke. It is the same blind spot behind stop_reason: refusal on Anthropic and mid-stream overloaded_error that arrives after a 200: the status code lies, and the truth is in the payload.
400 moderation rejection or an invalid_prompt error. Those fail loudly with a non-2xx status your error handling already catches. content_filter on a 200 is the dangerous case precisely because it does not.
How to detect and handle it
- 1. Log
finish_reasonon every completion - Not just on errors. If your logging only captures non-2xx responses, you will never record a single one of these. This is the highest-leverage change.
- 2. Alert on the rate, not the instance
- A single
content_filteris noise. A spike in the rate means a prompt pattern, a model change, or a new user behavior, and that is what you want to know about. - 3. Treat it as a failure path in code
- Do not render an empty string to the user. Handle
finish_reason: content_filterexplicitly: retry with an adjusted prompt, fall back to another model, or surface an honest message. - 4. Correlate with model and prompt
- The same prompt that returns
stopon one model can returncontent_filteron another, reasoning models especially. Track which model plus prompt shape trips it so you can route around it.
How Dstl8 detects this
Dstl8 reads the actual content of every log line, not just its status code, so a finish_reason: content_filter gets caught even though the request came back as a clean 200. We track how often it is happening and how that rate moves over time. When it climbs after a deploy or a model swap, Möbius flags it and ties it back to the change that introduced it. The failure your status dashboard cannot see becomes a first-class signal with a root cause attached.
Related patterns
References
- OpenAI API reference: error codes: defines
finish_reason: content_filter(“content was omitted due to a flag from our content filters”). - Microsoft Q&A: why filtered responses return HTTP 200: content filtering is part of the inference pipeline, not request validation, so a filtered response is still a 200. Rely on response fields, not status codes, to detect it.
- openai-node issue #320: developers hitting
content_filterin the wild.
Third-party API behavior, verified July 2026. Providers change filtering behavior over time; re-checked quarterly.
Your monitor says 200. Your users got nothing. See the difference.
Dstl8 surfaces the failures your status dashboard can’t: silent refusals, content filters, mid-stream errors. Cited to the log line.














