willianpinho.com Blog
Cover image for A success code that hides a failure

A success code that hides a failure

An HTTP 200 means the request came back, not that the work happened. A rotated token, an alerting path that went silent for days, and why the same trap gets worse once agents are wired to real tools.

Spent an hour today chasing why I'd missed an alert, and the answer bugged me enough that I'm writing it down.

I run a small alerting setup. When something on my infra trips, a bot posts a message into a chat channel so I actually see it. The send call returns HTTP 200. My health metric watches for non-200s and it had been flat green for weeks, so I figured the alerting was fine.

It wasn't. Nothing had been delivered for days.

The token the bot uses had been rotated and I'd forgotten to update it in one place. So the send call was hitting the API with a dead token. And here's the part that got me: the API returned 200 OK anyway. The actual failure was sitting in the JSON body: an ok: false with an invalid_auth error code. The status line said success. The body said "no."

My metric only ever looked at the status code. So every dashboard I had said "delivered." The reality was that the one system whose entire job is to tell me when things are broken had quietly stopped working, and the thing watching it was looking at the wrong field.

Why this isn't just a chatbot quirk

The reason I bothered writing this is that the same trap is everywhere once you start wiring agents to real tools, and it's worse there than it was for me.

A model gateway can return a 200 with a body that's a refusal, a truncated response, or a fallback model's output that's nothing like what you asked for. The HTTP layer is happy. The content is degraded and nobody downstream knows.

It gets worse inside an agent loop. A tool call can come back "succeeded" having done nothing at all, because what succeeded was the call completing, not the thing you wanted to happen. The agent reads "done," moves to the next step, and you end up with a clean run that accomplished none of what it claimed.

One nuance worth keeping straight: the chat API tunneling an auth error through a 200 is arguably a spec deviation. A gateway returning 200 on a refusal isn't. That's correct HTTP. The inference really did succeed; the content just isn't what you wanted. Different causes, same trap waiting for you downstream.

A 200 tells you the request was received and came back. It says nothing about whether the work you wanted actually happened. For most plumbing that gap never hurts you. But anywhere a silent no-op compounds, like an alerting path, or an agent stacking step on step, or a payment, it'll cost you, and you won't notice until later.

So the fix isn't clever. It's just: stop trusting the status code as proof of success. Reading the body is the start, not the finish, because for a multi-step agent the body can lie too. Assert on the effect you actually wanted, not on the fact that the request came back. If the work was "deliver a message," check that a message was delivered. If it was "the agent applied the change," go look at the external state: the DB row exists, the file on disk actually changed, not just whatever the response claimed.

A green light that isn't wired to the actual outcome is worse than no light. At least with no light you'd go look.