QueryPort

The response said 200. The body said no.

A status code describes delivery, not outcome. A service answering 200 with an error field is indistinguishable from success to any check that reads the code. What that cost us: retries that never fired, a failure rate stuck at zero, and a log entry naming the wrong cause.

A status code describes delivery, not outcome. 200 means the request arrived, was understood, and a body came back. What is in that body, the code does not say. To a check that reads status === 200, a service answering 200 with an error field inside looks exactly like a service that worked.

Our case: a widget that invented the cause

On 8 August 2026 a widget on this brand's site got a 200 back from our gateway with an empty result. The handling code read only result, did not find it, and told the visitor the generation had been cut off.

That was fiction. The response carried an error field with the real reason, and the real reason had nothing to do with anything being cut off. One log line listing the response keys settled it: what arrived was error, status, log_id, and result was absent entirely.

What it actually broke

  • Retries never fired. The retry logic branched on the status code. A refusal delivered as 200 triggered nothing, so the request was simply lost.
  • The failure rate read zero. Monitoring counted responses with a code of 400 or above. Refusals arriving as 200 never entered that share, so the chart was perfectly flat through a live outage.
  • The log named the wrong cause. A single catch for every reason produced a single explanation — the one a developer had written, not the one that happened. Searching the log by it found nothing, because nothing had happened by that name.

The third of those is what made the day expensive. A lost request is one request. A log entry that states a cause with confidence sends the next person looking in the wrong place, and it keeps doing that until somebody distrusts it on purpose.

The smallest reproduction

Open your client for any external service and find where it decides whether the call worked. If that decision looks like if (response.ok) or if (code === 200) and ends there, the reproduction is finished — there is nothing left to look for.

You can check this without breaking anything: paste a real failure response from your own service into the tool below. If it arrives with a 200, you are already in this situation.

Judge a response

Paste a status code and a body, and we will say whether it is success or a refusal. Your browser does the work: nothing is uploaded, and no address you type is fetched.

What to change in your code

  • Decide success from the BODY, and use the status code as a first filter. That order, not the other one: explicit refusal markers in the body first, code second.
  • Read the error field FIRST. If you look for the payload first and do not find it, you will report its absence as the cause — and lose the real one, which was sitting right next to it.
  • Empty success is not success. Status 200, no error field, but no payload either: there is nothing to show the person waiting, so it is a failure.
  • Log the response keys. One line listing which fields arrived finds the cause faster than any message written in advance ever will.
  • Tell the user honestly that no answer came. Naming a cause you do not know costs more than naming none: a confident wrong diagnosis sends the next person looking in the wrong place.

Where this note does not apply

  • Streaming responses (SSE, chunked) cannot be judged from a single body: the refusal arrives inside the stream, sometimes in the last event.
  • Services with their own vocabulary. The field may be ok: false, code: "FAILED", or a status number nested in the body. The tool does not know those — it catches the common names and says so in its own verdict.
  • Partial success. Batch endpoints return an array where some elements succeeded and some did not. Such a response has no single verdict at all, and any tool that produces one is misleading you.

Written by QueryPort Engineering. The incident is our own, 8 August 2026, on this brand's public endpoint. Found a mistake — tell us.

Read next

If your failure counter reads zero while users keep complaining, it is almost certainly counting status codes. The fix is not a dashboard — it is one decision moved from the code to the body, and one log line listing what actually arrived.

Our own gateway answers this way on purpose: a refusal comes back with a status that matches it, and every response carries a reference code you can quote back to us to find the exact log line behind it.

How the gateway answers · Ask us something specific