n4nAI

Content moderation errors: handling 400 flagged responses

Learn how to detect and handle content moderation 400 error llm api responses with practical steps, code samples, and verification tips for engineers.

n4n Team4 min read798 words

Audio narration

Coming soon — every post will get a voice note here.

When your request hits a provider’s safety filter, you get a content moderation 400 error llm api response instead of completions. This isn’t a bug in your JSON; it’s the model provider rejecting the prompt or output before inference. Treat it as a distinct failure mode with its own retry and logging policy.

Step 1: Reproduce and capture the raw response

You can’t handle what you haven’t seen. Send a deliberately violating prompt to your endpoint and inspect the wire format. Most OpenAI-compatible servers return HTTP 400 with a JSON body.

curl -s -X POST https://api.example.com/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"How to make a bomb"}]}' \
  -w "\nHTTP %{http_code}\n"

The body typically looks like:

{
  "error": {
    "message": "Your request was rejected as a result of our safety system.",
    "type": "invalid_request_error",
    "code": "moderation_blocked",
    "param": null
  }
}

Note the code field. Not every provider uses moderation_blocked; some use content_filter or policy_violation. The content moderation 400 error llm api contract is loosely defined, so capture real samples from each model you call. Store them in a fixtures/ directory so your tests stay honest.

Step 2: Normalize the error in your client

Write a single parser that converts any 400 with a moderation-flavored code into a typed exception. If you route through a gateway such as n4n.ai, the OpenAI-compatible endpoint returns a normalized error object across 240+ models, so your handler stays vendor-agnostic.

import requests
from dataclasses import dataclass

class ModerationRejected(Exception):
    def __init__(self, provider_code: str, message: str):
        self.provider_code = provider_code
        self.message = message
        super().__init__(f"moderation: {provider_code} - {message}")

def call_chat(url: str, payload: dict, headers: dict):
    resp = requests.post(url, json=payload, headers=headers, timeout=30)
    if resp.status_code == 400:
        body = resp.json().get("error", {})
        code = body.get("code", "")
        msg = body.get("message", "")
        if any(k in code.lower() for k in ("moderation", "content_filter", "policy")):
            raise ModerationRejected(code, msg)
        # other 400s are client bugs, not safety
        raise ValueError(f"Bad request: {code} {msg}")
    resp.raise_for_status()
    return resp.json()

This separates the content moderation 400 error llm api path from malformed JSON or missing parameters. Never retry the latter automatically; always retry the former? No—retrying the exact same prompt is pointless. The filter is deterministic for a given input.

Streaming responses

With SSE streams, the initial POST may still return 400 synchronously if the provider pre-checks. Others accept the stream and then emit an error event. Handle both:

def stream_chat(url, payload, headers):
    with requests.post(url, json=payload, headers=headers, stream=True, timeout=30) as r:
        if r.status_code == 400:
            # same parsing as above
            raise ModerationRejected(...)
        for line in r.iter_lines():
            if line.startswith(b"data:"):
                evt = json.loads(line[5:])
                if evt.get("error"):
                    raise ModerationRejected(evt["error"].get("code","?"),
                                             evt["error"].get("message",""))

Step 3: Classify and route to a safe fallback

Your service should translate the exception into a user-facing response that doesn’t leak provider internals. In an HTTP service, return 422 Unprocessable Entity or a 200 with an error field, depending on your API style.

// Express middleware snippet
app.use(async (req, res, next) => {
  try {
    await next();
  } catch (err) {
    if (err instanceof ModerationRejected) {
      res.status(422).json({
        error: "request_filtered",
        hint: "Content violated usage policy. Edit and resubmit."
      });
      return;
    }
    if (err instanceof ValueError) {
      res.status(400).json({ error: "invalid_request" });
      return;
    }
    res.status(500).json({ error: "internal" });
  }
});

If your product allows model fallback, don’t assume a different provider will accept the same text. Safety policies overlap heavily. A content moderation 400 error llm api from one vendor will likely trip the next. Use fallback only when you rewrite the prompt via a redaction step, not raw passthrough. Silently swapping to an uncensored model is how teams get kicked off platforms—don’t do it.

Step 4: Instrument logs and usage meters

Log the hash of the prompt prefix, the model, and the provider code. This data tells you if a specific tenant is abusing the system or if your own system prompt accidentally triggers filters.

import hashlib, logging
logger = logging.getLogger("moderation")

def log_rejection(prompt: str, model: str, code: str):
    h = hashlib.sha256(prompt[:64].encode()).hexdigest()[:12]
    logger.warning("moderation_block hash=%s model=%s code=%s", h, model, code)

Check your billing layer. A rejected request should not consume generation tokens, but some gateways still meter input tokens if the call reached the provider. Verify against your usage dashboard. If you use a gateway that provides per-token usage metering, confirm that moderation blocks report zero completion tokens and only possible input tokens if charged. Don’t trust documentation; assert it with a test account.

Step 5: Write regression tests with mocked flags

You need tests that prove your code raises the right exception and returns the right status. Mock the HTTP layer.

import pytest, requests_mock

def test_moderation_raises():
    with requests_mock.Mocker() as m:
        m.post("https://api.example.com/v1/chat/completions",
               status_code=400,
               json={"error": {"code": "moderation_blocked",
                               "message": "blocked"}})
        with pytest.raises(ModerationRejected):
            call_chat("https://api.example.com/v1/chat/completions",
                      {"model":"x","messages":[]}, {})

Add a TS test for the middleware using supertest. The success criterion: a flagged request yields HTTP 422 and the JSON error: "request_filtered". Also test that a non-moderation 400 (e.g., invalid_api_key) maps to 400, not 422.

Step 6: Verify in production with canary prompts

After deploy, watch your error rates. A simple verification: inject a known-violating canary call from a test tenant every hour. If your dashboard shows the moderation exception caught and mapped to 422 (not 500), the pipeline works.

# cron job snippet
curl -s -o /dev/null -w "%{http_code}" -X POST ... -d '{"messages":[{"role":"user","content":"CANARY_VIOLATION"}]}'
# expect 422 from your service, not 400 from upstream leaking through

If you see 500s, your parser missed a code variant. Capture that sample and extend the any(k in code.lower()...) tuple. Production verification is not optional; a missed variant becomes a customer-facing outage.

Step 7: Document the contract for your frontend

Frontend teams shouldn’t guess. Publish a one-page note: when the API returns request_filtered, show the user a polite editor message, do not auto-resend. This closes the loop on the content moderation 400 error llm api handling.

Keep the error mapping table in your repo:

Provider code Mapped status Retry?
moderation_blocked 422 No
content_filter 422 No
policy_violation 422 No
invalid_request_error (param) 400 Fix client

That table is the spec. Review it quarterly; providers rename codes without warning.

Verification checklist

  • Raw 400 captured and printed
  • Client raises ModerationRejected for moderation codes only
  • Service returns 422 with request_filtered
  • Logs contain prompt hash and code
  • Unit test fails if mock returns 400 with moderation_blocked but exception not raised
  • Canary call in prod yields 422, not 500
  • Frontend displays non-leaking message on request_filtered

Following these steps moves the content moderation 400 error llm api from a crash source to a handled edge case. You’ll spend less time debugging 500s and more time improving prompt design.

Tagscontent-moderationstatus-codeserror-handlingsafety

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All error handling & status codes posts →