Short answer
Over one week I found five separate things broken on this site. Not one of them
raised an error. Every single one reported success, and every report was
true — just narrower than I was reading it as. The upload really did succeed.
The plugin really was enabled. Those four endpoints really were closed. The
mistake was mine: I kept reading a subsystem’s status as an answer to my
question. This post is the pattern behind all five, and the one habit that
catches it.
The five
Each of these has its own post. Here they are in one line each.
| What I believed | What was true |
|---|---|
| The page is dying for no reason | A library had written a plain-English fix instruction that nothing was configured to show me |
| My images are fine, they render | Uploads succeeded and produced zero resized copies, for weeks |
| I closed every username leak | I closed every endpoint. The fifth leak was markup |
| My security plugin has stopped working | It was working perfectly; my own filter was swallowing everything it said |
| These plugins are active | Two of them were active and accomplishing nothing at all |
Five different subsystems. Five different causes. One symptom, which was no
symptom.
The pattern is not lying software
It is tempting to file this under “WordPress is sloppy”. That is the wrong
conclusion and it will not help you, because each of those systems reported
accurately on the thing it was responsible for.
- The media handler said the upload succeeded. It had. Generating derived
sizes is a different step, and it failed separately and silently. - The plugins screen said the plugin was enabled. It was. Whether it then
did anything is not a question that screen is answering. - My hardening said those endpoints were closed. They were. The theme’s
byline was never an endpoint. - The login form said the attempt failed. It had. Whether a limiter was
counting is invisible until you cross its threshold. - PHP said nothing at all, because nothing on that server was configured to
receive what it had to say.
Every status is scoped to a subsystem. I was reading each one as scoped to my
goal. That gap — between the thing that reported and the thing I actually cared
about — is where all five of these lived.
Once you see it that way the failures stop looking like bad luck and start
looking like the same bug repeated, which is much more useful, because a
repeated bug has a repeatable defence.
Why silence is the worst signal
There is a second thread running through this, and it is worth separating out.
An error message is a claim. It might be wrong or misleading, but it has content
and you can act on it. Silence has no truth value. It is equally consistent
with:
- the thing worked
- the thing failed
- the thing never ran
- the thing ran, failed, and had nowhere to report to
We treat silence as positive because in well-instrumented systems it usually is.
A self-hosted WordPress on a minimal PHP install is not a well-instrumented
system. Logging may be off. Mail may be unconfigured, so the failure notices
never arrive. Half the interesting operations return 200 regardless of outcome.
In that environment “no error” tells you almost nothing, and treating it as
reassurance is precisely how a broken image pipeline survives for weeks in front
of someone who looks at the site every day.
The habit that catches all five
Here is the whole method, and it is one question asked at the right moment:
Before you believe a fix worked, name the observable that would be different
if it had.
Not “does it look fine”. Name the specific thing — a file that should now exist,
a header that should now be absent, a request that should now be refused, a value
you can read back. Then go and look at that specific thing, from outside the
site, logged out.
If you cannot name an observable, you have not verified anything. You have
stopped worrying, which feels identical from the inside and is not the same
thing at all.
This sounds obvious written down. It was not obvious in the moment, five separate
times, which is why it is worth writing down.
It is also the habit that matters most once something other than you is doing the
work. An agent that just edited a file will tell you it edited the file — a true
statement, and a different claim from the one you actually care about.
That working style, and the rest of what shipped this site, is here.
What the observable usually is
| The question | The observable | How you check it |
|---|---|---|
| Did the code even run? | A side effect you can read back | Write a value somewhere and fetch it |
| Did it produce output? | The artefact exists | Ask for the file, the size, the row |
| Is it reachable from outside? | The response itself | Request it logged out, not from the admin |
| Is it actually enforcing? | A refusal | Cross the line deliberately, when it is safe |
| Did my write stick? | The stored value | Read it back; a 200 is not a receipt |
That last row is the one I would tattoo on something. A write that returns 200
has told you the request was accepted, not that the value you sent is the value
now stored. On this site those two things came apart repeatedly.
The failure modes this catches
Each row of that table maps onto one of the five, which is a decent sign the
method is real rather than a tidy story told afterwards:
- Read it back, do not trust the status would have caught the thumbnails on
day one — the metadata was sitting there the whole time saying zero sizes. - Look from outside, logged out is what eventually found the username in the
byline, after weeks of looking at the same page while logged in. - Cross the line deliberately is the only thing that could ever have told me
whether the login limiter was alive. - Write a value and fetch it is exactly the shutdown handler that finally
surfaced the fatal error. - Ask for the artefact is the two-request check that showed the page cache
was serving nothing.
Same method, five shapes.
Where this stops being a WordPress problem
Nothing above is specific to WordPress. WordPress just gives you a lot of chances
to practise, because it is a large system assembled from parts that report
independently and were never asked to agree with each other.
The same failure at scale is what most postmortems are made of: a component that
was healthy by its own definition, inside a system that was not, and a dashboard
full of green that was answering a question nobody had asked. The difference is
that on a small self-hosted site the cost of learning this is an afternoon and
some embarrassment, rather than an incident review.
Take the cheap version of the lesson.
FAQ
Why does WordPress report success when something failed?
Usually it does not. It reports success for the operation it actually performed,
which is a narrower claim than the one you are reading it as. An upload really did
succeed even when no thumbnails were generated, because generating them was a
separate step that failed quietly alongside it.
How do I verify a WordPress fix actually worked?
Before you believe it, name the observable that would be different if the fix had
worked, then go and look for that specific thing from outside the site while
logged out. If you cannot name an observable, you have not verified anything —
you have only stopped worrying about it.
Is no error message a good sign in WordPress?
No. Silence is compatible with success, with failure, and with a step that never
ran at all. It carries no information on its own. On a self-hosted install where
logging may not be configured, treating silence as a positive signal is how
problems survive for weeks.
Wrapping up
Five failures, five subsystems, one reading error repeated five times. None of the
software lied to me. It answered the question it was asked, and I kept asking the
wrong one.
The fix is not more vigilance, which does not scale and which I do not have. It is
one question, asked before you close the tab: what would be different if this
had worked, and have I looked at that?
Everything on this site that stayed broken, stayed broken because I never asked
it.
Primary sources:
error_get_last
· WordPress REST API
· Debugging in WordPress