Skip to main content

status-code-is-not-a-reason-code

An endpoint that refused for exactly one reason gets a client that keys its message off the HTTP status. Add a second refusal reason and that mapping shows the WRONG actionable hint — plausible, silent, and reported by nobody. Before adding a refusal, open the existing client mapping; ship the reason as a code in the body, and take any number in the copy from the response.

Ir para a instalação

Informações da origem

Repositório
blas1n/claude-skills
Última atividade na origem
3 de setembro de 2026 às 08:09
Idioma detectado do SKILL.md
inglês
Estrelas
2
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
status-code-is-not-a-reason-code
description
An endpoint that refused for exactly one reason gets a client that keys its message off the HTTP status. Add a second refusal reason and that mapping shows the WRONG actionable hint — plausible, silent, and reported by nobody. Before adding a refusal, open the existing client mapping; ship the reason as a code in the body, and take any number in the copy from the response.
version
1.0.0
category
trap
# The status code stops being an identifier at the second refusal reason ## When to use You are about to make an existing endpoint refuse for a **new** reason — a quota, a plan limit, a feature flag, a state precondition — on a surface that already refuses for something else. Trigger words: "also reject when…", "add a 400 for…", "cap this", "gate this behind the plan", "return 409 if…". ## Problem While an endpoint had exactly one refusal cause, keying on the status was correct, and it reads as correct forever: ```tsx // 400 here can only mean "this workspace has no products" setError(err.status === 400 ? t("errorNoProduct") : t("errorSend")); ``` Add a second cause with the same status and the branch answers confidently and **wrongly**: a founder with a dozen products is told to create one. What makes this worse than an ordinary bug: * **No error, no red, no log.** The user sees a fluent, localized, plausible sentence. It is simply about the wrong thing. * **The new code looks fine.** The defect is in a file the PR never opens — a client written months earlier under an assumption the PR just invalidated. * **Tests stay green.** The old mapping's test still passes (its cause still maps correctly); the new refusal's test asserts the status, which is right. Nothing asserts *which sentence a user sees for the new cause*. ## Recognizing it Grep the client for branches on a bare status before adding the refusal: ```bash rg "status === (400|403|409|422)" --type ts rg "\.status_code == (400|403|409)" # server-side callers too ``` Any hit whose comment or copy names **one specific cause** is a landmine the moment a second cause shares that status. ## The fix 1. **Open the existing mapping in the same PR.** This is the whole discipline. The grep above takes seconds; skipping it is how the defect ships. 2. **Send a reason code, not just a status.** FastAPI's `detail` accepts an object: ```python raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail={"code": "run_cap_reached", "limit": limit, "held": held}, ) ``` The client branches on `err.reason?.code`, never on the number alone. 3. **Pick a status that is not already spoken for**, as a second line of defence — 429 for a quota, 402 for a plan wall. But do not *rely* on it: the next reason will collide with something. 4. **Any number in the copy comes from the response.** A hard-coded "3" lies for every tenant on a different limit. Pin it: ```tsx it("takes the number from the response, not a hardcoded free-plan 3", ...) // respond limit: 10 → the sentence must say 10 ``` 5. **Do not render the backend's `detail` string verbatim** on a localized surface — that is how English lands on a Korean screen. The server sends the code and the numbers; the client owns the sentence. 6. **Keep a control test for the OLD refusal.** Without it, deleting the original branch leaves every new test green. ## Verification Cut each wire separately and watch only its own tests go red: | cut | expect red | expect still green | |---|---|---| | remove the new reason branch in the client | new-reason tests | the old-cause control | | unwire the refusal on surface A (REST) | A's tests | surface B's tests | | unwire the refusal on surface B (MCP/CLI) | B's tests | A's tests | If one cut reddens both surfaces, they share a guard and one of them is not actually wired — the classic mirrored-surface drift. ## Measured case (BSVibe, 2026-09-03) `POST /api/v1/messages` had refused only for a zero-product workspace (400). Adding a free-plan concurrent-run cap as another 400 would have shown *"Create a product first"* to founders with products. Shipped instead as 429 + `{code, limit, held}`, with the client branching on `code` and the limit interpolated from the response. ## Related * `single-active-resolver-degrades-on-new-account-class` — same shape one layer down: a discriminator that was unique stops being unique when you add a second class of the thing. * `mirrored-surface-drifts-in-the-direction-of-least-testing` — share the RULE, keep the error faces separate, test both.
Ver no GitHub