Chapter 6 of 8 · 14 min read
Error copy for a blocked request
On this page
Two strings are the usual candidates for what a user sees when a platform content filter refuses their request:
A. Something went wrong. Please try again in a moment.
B. Your request was blocked by our content policy (category: violence).
A is almost always the incumbent, because it is what component libraries ship as their default error state and it predates there being a model in the product. B is what somebody proposes after an incident in which A told a group of users nothing for over a week. Support usually holds a third position, argued well: showing the category would let users self-serve, since every ticket on this error class is a five-minute call that ends in "I'm sorry, I can't tell you why".
All three positions are about the same underlying question, which is what a product is allowed to claim about a decision it did not make. The way out is to stop choosing between candidate sentences and start from the properties of a platform block that are actually true, letting the copy fall out of them — which is also the only way to notice that a model refusal, a genuinely different failure from the first chapter of this topic, must not be given the same sentence.
Try again was written for transient failures
Something went wrong, please try again is inherited copy, and the inheritance explains why it feels safe. It was written for 500s and timeouts: a class of failure that is transient by construction, where the server's state genuinely differs a second later, and where "try again" is both true and the correct instruction. Every guideline on graceful degradation — the practice of failing into a reduced but still usable state rather than a blank screen — encodes that assumption, because for two decades essentially every server-side failure a user could see was transient.
A filter block breaks the assumption. The verdict is a function of the input, so the same bytes produce the same decision and the retry the copy prescribes is the one action guaranteed to fail. On an input-stage block that costs the user a few seconds and their patience. On an output-stage block it costs the full generation again — a third attempt regenerates a completion you pay for and then discard — once per press of a button the interface is actively encouraging them to press.
The second failure is diagnostic rather than economic. A user told "something went wrong" who retries three times concludes the software is broken, and reports it that way. That report routes to whoever owns availability. Meanwhile the actual condition — a policy classifier rejecting a category of legitimate professional language — is in nobody's queue, because nothing in the user's report contains the word policy. A misrouted incident stays misrouted for as long as the copy keeps describing it wrongly.
That chain is worth following link by link, because it is the reason error copy is an engineering concern rather than a writing one. A user reports what they saw, and what they saw was a sentence you wrote. The words in their report are therefore your words, repeated. Support routes tickets by matching those words against queues, since that is all a first-line agent has to go on. So the queue a ticket lands in is decided by the copy, one step removed. A card that says something went wrong routes to whoever owns things going wrong, which is availability, and availability will investigate the platform, find it healthy, and close the ticket — correctly, on the evidence they were given. Nothing in that sequence involves anybody being careless, which is why it repeats for weeks.
So A is not merely unhelpful. It is a false statement about the nature of the failure that generates the wrong user behaviour and the wrong internal signal, and its familiarity is exactly why it is rarely questioned.
Where naming the category goes wrong
B is honest, and honesty is not the objection. Four things are.
It attributes the decision to you. Our content policy tells the user that your company inspected their document and judged it unacceptable. That is not what happened — the provider's classifier did, on infrastructure you rent, under a policy you did not write and cannot inspect — and in a regulated domain that difference is one the customer's compliance team will want to talk about. Positioning yourself as the author of a policy you have no visibility into is a commitment you cannot honour when they ask to see it.
The category is frequently wrong from the user's point of view. A clinical
triage block fires on self_harm; the clinician's note is a self-harm risk
assessment. Telling them "blocked: self-harm" reads as an accusation about a
document whose entire purpose is the assessment of self-harm risk. Provider
categories are coarse labels on a classifier's output, not statements about
intent, and rendering them into a user-facing sentence borrows a precision they
do not have.
It publishes an oracle, in the sense the word carries in security work: a service that will answer questions about a hidden rule for anybody who cares to ask. A message naming the category that fired turns the product into a free, unauthenticated interface for probing the provider's classifier — submit, read the category, adjust, resubmit. That is a meaningful gift to the small fraction of users actually trying to get prohibited output, and the fraction is not zero on any consumer surface.
The step from "the screen says a word" to "the classifier has been mapped" is
short enough to be worth walking, because without it the objection sounds
theatrical. A bare refusal gives an attacker one bit per attempt: blocked, or
not. That is enough to find the boundary eventually and it is slow, because
every failed guess looks like every other failed guess. A named category gives
them a labelled signal instead. Now a change that swaps violence for
self_harm is informative, a change that leaves the category alone tells them
the edit was irrelevant, and a change that clears the block tells them which
direction to keep moving in. What was a search with no feedback becomes a search
with a gradient, and the same reduction loop that earlier in this topic took a
clinical note from fourteen hundred characters to sixty-eight in eleven probes
works just as well pointed the other way. The technique is not the problem. The
signal you hand it is.
And it creates a support contract you cannot fulfil. Once the category is on the screen, users ask what it means, why it fired, and what the threshold is. You do not know. The answer to every one of those is "the provider does not tell us", which is a worse conversation than the one support is having now, because it happens after you appeared to offer an explanation.
Deriving the message from what is actually true
Four things are true of a platform block, and each is something the user needs. The request cannot be served, in a way that is settled rather than pending. It is not the user's fault in any sense they can act on; a legitimate professional document is not misconduct. Rewording will not reliably help, because the user cannot see the boundary and guessing at it wastes their time and your tokens. And there is a route to a human, because for a real workflow the answer to "I cannot complete my task" has to be a person rather than a dead end.
None of those four is a style choice, and each one is the end of a chain this topic has already built. Settled rather than pending follows from the verdict being a threshold comparison on a fixed input: nothing is in flight, so there is nothing to wait for. Not the user's fault follows from what the classifier does, which is score text rather than assess a person — it has no notion of who sent the document or why, so it cannot have found them culpable of anything. The route to a human follows by elimination: the user cannot change the verdict, the interface cannot change the verdict, and the only parties who can are the ones who can reconfigure a deployment or file an appeal, none of whom is reachable from an error card.
The third clause is the one that carries the most weight and the least obvious reasoning, so it is worth stating why rewording is unreliable rather than useless. A block means the input's score crossed a threshold, and the user can see neither quantity. Naming the category would tell them which axis they are on and still not tell them how far over the line they are, or which words are carrying the score, or how much they would have to change. Some rewrites will work, because some inputs sit close to the boundary, and the ones that work will teach the user a rule that does not generalise to the next document. "Rewording won't help" is therefore not quite literally true and is the right thing to write: it is accurate about the expected outcome, and the alternative sentence — one that invites rewriting — turns a settled failure into an unbounded amount of the user's time. The comparison below scores the three candidate messages against those four requirements, and its third column is what they produce.
| A: "try again" | B: name the category | C: derived | |
|---|---|---|---|
| Accurate about the failure | No — implies transience | Partly — attributes it to you | Yes |
| Prescribes an action that can work | No — retry cannot succeed | Sometimes, by guesswork | Yes — escalate to a person |
| Wastes tokens and quota | Yes, once per retry | Some, via rewrite attempts | No |
| Implies the user did something wrong | No | Yes | No |
| Exposes the policy surface | No | Yes | No |
| Gives support something to work with | No | A category with no context | A reference id |
| Routes the incident correctly | No — reads as an outage | Partly | Yes |
The message that satisfies the column on the right:
We can't produce a response for this request. It isn't something you did wrong, and rewording it won't help. If you need this completed, contact support with reference RB-7QK4-22.
Each clause is doing work the review can defend. Can't produce a response is a statement about outcome rather than cause, so it is true whichever layer refused and whichever category fired. Isn't something you did wrong removes the accusation without denying that a policy exists. Rewording won't help is the clause that saves the retries, and it is true for the vast majority of blocks; a user determined to rephrase will still try, and that is fine, because you have not instructed them to. The reference maps to the structured block record carrying provider, category, deployment, endpoint and the provider's own request id.
Support's argument is answered by that last clause rather than by the category.
Their ticket volume is high because every ticket is a dead end: the user has
nothing, support has nothing, and the call ends in an apology. With a reference,
the call ends in a lookup. The engineer on rotation sees within seconds that this
is the known self_harm block on the clinical deployment, that it is fixture
PB-2026-08-04-01, that an appeal is open, and that the customer-specific
workaround is documented. That is a shorter call than the self-service one
support imagined, and it does not require the user to understand anything about
classifiers.
Wiring it without collapsing the distinction
The message depends on the error taxonomy, so resolution has to happen from the error class rather than from a string match, and — this is the part that gets lost in implementation — a model refusal must not receive the block message.
type UserFacing = { key: string; reference?: string; canRetry: boolean };
export function messageFor(err: AppError, ref?: string): UserFacing {
if (err instanceof PolicyBlock)
return { key: "error.policy_block", reference: ref, canRetry: false };
if (err instanceof ModelDeclined)
return { key: "error.model_declined", canRetry: true };
if (err instanceof TransientUpstream)
return { key: "error.transient", canRetry: true };
return { key: "error.unknown", reference: ref, canRetry: false };
}{
"en": {
"error.policy_block": "We can't produce a response for this request. It isn't something you did wrong, and rewording it won't help. If you need this completed, contact support with reference {reference}.",
"error.model_declined": "The assistant declined to answer this one. Adding context about what you need it for often helps.",
"error.transient": "That didn't go through. Try again in a moment."
},
"es": {
"error.policy_block": "No podemos generar una respuesta para esta solicitud. No es culpa tuya y reformularla no servirá. Si necesitas completarla, contacta con soporte indicando la referencia {reference}.",
"error.model_declined": "El asistente ha preferido no responder a esto. Añadir contexto sobre para qué lo necesitas suele ayudar.",
"error.transient": "No se ha podido enviar. Inténtalo de nuevo en un momento."
}
}canRetry is the flag the interface reads to decide whether to render a Try
again button at all. Shipping honest copy while leaving the retry button in
place undoes most of the benefit, because the button is a stronger instruction
than the sentence above it.
The model_declined string is why the taxonomy had to survive into the
presentation layer. A prose refusal genuinely is influenced by how the request
is framed, so "adding context often helps" is true advice there and false advice
for a block — the same sentence, correct in one branch and harmful in the other.
A single generic error message cannot be right for both, which is the
presentation-layer form of the argument the first chapter made about metrics.
The reference, and the machine-readable twin
The reference deserves a few minutes of design rather than a UUID rendered
small. It gets read aloud over a phone by someone already frustrated, so it
wants an alphabet without the characters people confuse — no O against 0, no
I against 1 — a length around eight characters, a prefix identifying what
kind of thing it is, and no encoded meaning that leaks a user or tenant
identifier. It maps to the structured block record and to nothing else, and it
should be searchable in whatever tool support actually opens, not only in the
log platform three teams have access to.
Where the product has an API as well as a user interface, the same distinction
has to survive into the error contract you publish. Programmatic clients need a
stable machine-readable code, because the alternative is that integrators write
regular expressions against your English message and break when you improve it.
A type of policy_blocked alongside model_declined and
upstream_unavailable, with a documented statement that policy_blocked is not
retryable, does for your customers' retry layers exactly what the taxonomy did
for yours. What the API response should not carry is the provider's category,
for the same reasons the card does not, with one narrow exception: an enterprise
integration where the customer has a contractual relationship with the
underlying provider and has asked for it. That is a per-tenant flag with an
owner, not a default.
Good copy does not restore the workflow
A user who reads a well-written explanation still cannot complete their task, and no copy edit changes that. Good error text converts an unexplained failure into an explained one and routes it correctly; it buys time and goodwill rather than function.
It is also not the right message everywhere. A self-serve consumer product with an adversarial minority may reasonably say less, because the population making blocked requests there is not shaped like a clinic's. And a batch pipeline has no user reading a card at all — the equivalent artefact is the per-document status in the job report, where "blocked by provider policy, reference RB-…, not retried" belongs, and where the failure is per-item rather than per-session.
The whole of this chapter is one substitution: a reference code in place of an explanation. It works because the four true things about a block are sayable without naming a category, and because the thing a blocked user most needs is not a diagnosis but a route to somebody who can act. That route is only real if the code on the card resolves to a record with the provider's category, stage and request id already attached, which is why the capture and the copy are one piece of work rather than two.
It is also not a substitute for telling the customer's organisation, as opposed to the individual user. Forty clinicians each seeing a well-worded card, individually, is still forty people discovering a systemic problem one at a time. The account owner needs a message the user-facing string cannot carry: how many requests are affected, since when, what has been asked of the provider, and what happens if the answer is no. Which means somebody has to know what asking the provider is actually worth.
Provider content filters as an operational problem — read the current version at https://daviddominguez.dev/learn/en/provider-content-filters-as-an-operational-problem/error-copy-for-a-blocked-request