An API that returns 200 and does nothing is worse than one that returns an error

I cross-post my articles to dev.to. Looking at the numbers, the posts tagged agents were getting traffic and the one without it had a single view in twenty hours.

Obvious fix: add agents to that post. I sent a PUT updating the tags.

The response was 200.

I opened the post. The tags were unchanged.

Three requests, three 200s, three identical responses

Assuming I’d malformed the request, I ran the smallest test I could: three PUTs to the same article, sending agents, then python,agents, then the original tags.

All three returned 200. All three returned byte-identical bodies — the tags the post was created with.

The truth: dev.to tags are immutable after publish, and the API silently ignores the field. Not a 403 saying you can’t do that. Not a 422 saying the field is read-only. A 200, and then nothing happens.

That one field made me wrong twice

The first time was the day before. I’d sent 4 tags and gotten 3 back.

My conclusion: dev.to caps tags at 3.

That conclusion is entirely reasonable. You send four, you get three, what else would it be? I was confident enough to write MAX_TAGS = 3 into a script comment as an established fact.

What actually happened: the tags field was never applied at all. What came back were the three tags from creation time. It had nothing to do with a cap. I could have sent one tag or ten and gotten the same three.

One silently ignored field, two wrong conclusions in two days, and I committed one of them to source control as documentation for my future self.

That’s the real cost. Not the failed request — the false fact I wrote down as knowledge.

Why 200 is more dangerous than an error

An error interrupts you. It forces a stop, and it usually tells you something true. Even when the message is imprecise, “this did not work” is accurate information.

A 200 doesn’t interrupt you. You tick the step off and move on. You proceed on a false premise, believing you verified it.

Going back through my ops log, this failure mode shows up more than once.

A 200 that means failure

When the site first went live I added a root landing page. Then I typed a URL that doesn’t exist — 200.

Cloudflare Pages falls back to index.html when there’s no 404.html. So every nonexistent URL served the homepage with a success status.

That’s a textbook soft 404, and it reports a failure using a success code. If my check had been “is the site up,” it would have passed perfectly. Adding 404.html restored real 404s.

One space, and the rule quietly disappears

A _redirects file is whitespace-delimited. Four of my legacy URLs contained spaces. Written literally, those lines parsed as 4-to-6 columns — and the rule silently died.

No warning, no parse error, deploy succeeds. You just find out later that four URLs don’t redirect, and if those four weren’t in your test set, you never find out at all.

I hit it again today, but saw it coming

Today I needed to confirm the site wasn’t blocking AI crawlers. I fetched robots.txt. Clean: User-agent: * / Allow: /.

Because of everything above, I didn’t stop there. CDN-level crawler blocking happens at the edge. It returns 403 without consulting robots.txt at all. Two different layers.

So I actually hit an article URL while impersonating ten different crawler User-Agents. Ten 200s. That’s what counts as verified.

A clean robots.txt does not mean you aren’t blocked. Checking only that buys you false calm.

Same day: a real number measuring the wrong thing

There’s a subtler variant, where the number is accurate but it isn’t counting what you think.

I connected CDN analytics and saw 633 requests to one article. GA4 reported 9 users over the same window. A 70× gap.

633 was true. Nobody lied. Breaking it down by User-Agent: 108 infrastructure prefetches, 64 from a monitoring bot, link-preview crawlers, a Google agent, and — my own verification scripts. Requests that might plausibly be humans: about 76.

Had I reasoned from 633, I’d have concluded that submission drove a traffic spike, and then anchored every downstream decision about topics and pricing to a signal that wasn’t there.

Postscript: it happened again while I was publishing this

With the article written, I ran the pipeline: generate the two social cards, build, deploy. All three steps succeeded. No warnings.

Then I requested the two image URLs on the live site — 404.

The cause was one line in the build script: cp -R root-assets/og dist/og. When the destination directory already exists, cp -R doesn’t copy the contents into it — it creates a subdirectory of the same name inside it. Every card had gone to dist/og/og/.

Nothing errored, because nothing illegal happened. cp succeeded, the build succeeded, the deploy succeeded.

Worse: I’d used the same pattern for the fonts and kit directories. All three were nested. The older articles’ cards were only in the right place because a clean build had once put them there.

I found it because of a rule in my own constitution — social cards are the thing most easily missed, so always request them for real. I wrote that rule after the last time this bit me.

This article demonstrated its own thesis during publication.

What I changed

1. Commit the refutation, not just the conclusion.

The MAX_TAGS = 3 comment is gone. But I didn’t simply replace it with the right answer — I left the whole thing in: what I believed, why it was wrong, what’s actually true.

I got this wrong twice. Writing it out so there isn’t a third time.

Because whoever reads that next (probably me, in a few weeks) will see only a correct answer and quite naturally repeat the same mistake somewhere else. The wrong reasoning is itself the asset.

2. Verify the result, never the response.

The 200 you get back only proves the server received something. To know the thing happened, you have to read it back — from the side a user would see it from.

Did the tags change? Re-fetch the article. Is the redirect live? Request that URL. Is the crawler allowed? Impersonate it and see.

3. Separate “this layer passed” from “the thing is true.”

robots.txt is one layer; edge blocking is another. Requests are one layer; readers are another. Verifying one layer and claiming the whole thing holds is how you manufacture the next wrong conclusion.


An API that returns an error costs you half an hour.

An API that returns 200 and does nothing gets a false fact into your docs, your comments, and your next plan — and then you make a much larger decision on top of it two weeks later.

The first is a failure. The second is contamination.