Tag: WordPress

  • Eight automated checks passed. The reader saw asterisks.

    Eight automated checks passed. The reader saw asterisks.

    For about a day, three of the four articles on this site displayed their own markup. Headings arrived as ## What the dashboard said, with the hashes sitting in the text. Bold arrived wrapped in asterisks. Links arrived as square brackets followed by a naked URL in parentheses.

    Every one of those articles had passed an eight-gate publication checker with no failures. The checker was working exactly as designed. That is the part worth writing down.

    What the reader actually got

    The left panel is a live article as published. The right is the same passage after the fix.

    Two screenshots of the same passage from our own published article. On the left, as it was live for about a day, the heading appears as hash characters and the bold text is wrapped in asterisks. On the right, after the fix, the heading renders as a heading.
    Our own published page, before the fix on the left. The reader met the markup before they met the sentence.

    How it happened

    Our publishing pipeline reads a file, runs the gates, then posts the body to the WordPress REST API. The articles were written in Markdown. WordPress does not render Markdown. It stores what you send it.

    The assumption was sitting in our own source code, in a comment on the data model:

    body: HTML or Markdown

    Nobody had ever tested the second half of that sentence. The first article on this site happened to be written in raw HTML, so it rendered correctly. When we switched to writing in Markdown, everything after it broke, quietly, in a way that looked fine from every angle we were checking.

    Why the checker did not catch it

    This is the useful part. Here is what the eight gates ask:

    Does every link resolve? Do commercial links point only to approved programmes? Are there banned expressions? Is this a near-duplicate of something already published? Is the disclosure above the fold? Are the editorial note and disclaimer present? Are there generation artifacts in the text? Are the dates internally consistent?

    Read those eight as a set. Every single one is a question about what the article says. Not one of them is a question about whether the article has a form.

    The nine questions our publication gates ask. The first eight are all questions about what the article says. Only the ninth asks whether the article has a rendered form.
    Written out as a set, the shape of the blind spot is obvious. Added one at a time after incidents, it never was.

    That was not an oversight in the sense of forgetting an item. Each gate was written in response to a specific failure we had already lived through: a dead link, a duplicate, an income claim in a headline. The list was a list of scars. You cannot write a scar for an injury you have not had yet, which is precisely why a checklist built from experience has a predictable shape: it is dense where you have been hurt and empty everywhere else.

    The gap was not in the list. It was in the space the list did not cover.

    Why a human did not catch it either

    We publish, then read the result of the publish. The API returned 201. The gate report printed eight passes. A person read both of those and moved on.

    Both of those signals were true. Both of them were about the system. Neither of them was about the page.

    Nobody opened the public URL. Not once, across three articles. The information was one click away for a day and a half, sitting in a browser tab nobody opened, because the dashboard had already said the thing was fine.

    This is the same failure we wrote about two weeks ago, where a site produced no revenue for fifteen months while the instrument measuring it was disconnected. Same shape. We read the report instead of the thing the report described. Knowing about the pattern in the abstract did nothing to stop us repeating it eleven days later.

    Three signals after publishing. An HTTP 201 means the request was accepted. Eight of eight passed means the body satisfied eight content rules. Only opening the public URL tells you what the reader sees.
    Two of these three signals were green for three articles running. Neither was about the page.

    What we changed

    A converter, so the input is always valid output. Markdown now becomes HTML before it reaches the API. A few hundred lines, standard library only, handling exactly the notation our articles actually use and nothing else.

    A ninth gate that fails closed. G-9 scans the body that is about to be posted for surviving markup notation. If it finds any, nothing publishes. Writing the gate took twenty minutes. It exists now because the failure happened, which is the same way the other eight got here.

    A second checker, at a different scale. The nine gates all look inside one article. We later built a set of checks for the space between articles, which is where our worst failures had been living.

    A rule that is not automated at all. After publishing, open the public URL and look at the page. Not the API response. Not the gate report. The page, as a reader loads it.

    That last one is the only part of this that generalises. The first two fix this specific bug. A machine check can confirm that a page contains no leftover asterisks. It cannot tell you that a paragraph is confusing, that a figure landed in the wrong place, or that the article now opens with two identical headlines because the cover image repeats the title. Every one of those was also true of our pages this week, and the only thing that found them was a person looking at the page.

    If you publish through an API

    Three things, in order of how much time they save.

    Load the public URL after every publish. The response code tells you the request succeeded. It tells you nothing about what the reader gets.

    Assume every comment in your own code is untested. The word or in a code comment is where bugs live. Ours said Markdown would work. It had never worked. It had simply never been tried, and the comment made it look like a decision rather than an assumption.

    When you add a check, ask what category it belongs to. Our eight gates were all one category. That is invisible while you are adding them one at a time in response to incidents, and obvious the moment you write them out as a list and read them together. If every check you have asks the same kind of question, you have a blind spot the exact size of the questions you are not asking.

    None of this made the articles better. It made them legible, which is the floor rather than the ceiling. But a reader who sees asterisks in the second paragraph does not reach the argument, so the floor is where the work has to start.


    Editorial note. Written and edited by the operator of this site. The screenshots are of our own pages, taken before and after the fix on 2026-09-14. The affected articles were live in the broken state for roughly one day.

    Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.

  • Three 403s in one week. We named the wrong cause twice, for the same reason.

    Three 403s in one week. We named the wrong cause twice, for the same reason.

    Every 403 we hit last week came from our own infrastructure. Not an attacker, not an outage — our own web application firewall, refusing our own requests, on our own site. That part is ordinary; anyone running a managed host with a WAF in front of WordPress will meet it. What is worth writing down is that we diagnosed two of the three wrong before we diagnosed them right, and both times the mistake had the same shape.

    We run a small English-language site on a Japanese managed host. The publishing pipeline is automated: a checker runs eight gates against a draft and posts it through the REST API if all eight pass. So when the firewall says no, it says no to a script, with no browser and no human watching. You get a 403 and an HTML page that tells you nothing.

    Table of three 403 errors from one week, showing what we concluded, what was actually true, and the one-minute check that would have shown it. Two of the three conclusions were wrong.
    Three 403s in one week. The column we would rather not publish is the second one.

    The first one: a login that worked everywhere else

    A login POST returned 403 while a GET of the same page returned 200. We concluded the site had a block specific to itself — it is the newer of our two properties, and it had an overseas-access restriction switched on that the older site did not. We turned that restriction off.

    It made no difference, because it was never the cause. The real cause was a separate setting in the host’s control panel, a login restriction on the admin screen, sitting one menu below the thing we had changed.

    What would have caught it in a minute: sending the same request to our other site. We did that eventually and got the same 403 there — from a site that was working perfectly well. The 403 was simply what that endpoint returns to a request with no session cookie. It was never evidence of anything.

    We had changed a security setting on a live site based on a difference we never confirmed was a difference.

    The second one: three paragraphs the firewall would not accept

    An article refused to publish. The gates passed; the POST returned 403. This one we got right, and the reason we got it right is worth naming: we did not reason about it at all. We posted the article one paragraph at a time. Forty-seven went through and three did not.

    The three had nothing in common thematically. They had something in common structurally: each contained a comma-separated list inside quotation marks. The firewall rule was a SQL injection filter that treats a comma followed shortly after by the word and as an attack signature — the shape of any ordinary three-item list written with an Oxford comma.

    We would print the example here, but the firewall will not carry it. This article is published through the same rule set, and an example is precisely what it refuses. This paragraph took four attempts.

    Bisecting is not a clever technique. It is just a comparison baseline applied repeatedly, which is why it works when reasoning does not.

    The third one: an upload limit that did not exist

    Uploading images to the media library, three succeeded and the fourth failed with 403. Every attempt after that failed too. The obvious reading was a rate limit — three per window, then a cooldown.

    So we waited, and retried eight times over ten minutes. All 403.

    Five diagnostic steps for a blocked image upload. A ten minute wait returned 403. A new filename returned 403. A re-render at two different sizes returned 403. Re-sending a file that had already uploaded returned 200, which ended the investigation.
    Ten minutes spent on a theory that three earlier successes had made look obvious.

    Then we did the thing we should have done first: took an image that had already uploaded successfully, gave it a new filename, and sent it in the same batch as one that kept failing.

    The known-good image returned 200. The failing one returned 403. There was no rate limit and there never had been. The block was a property of the file, not of the sequence — certain PNG byte sequences match a firewall signature, and re-rendering the same figure at a different size produced the same bytes in the same place. Converting it to JPEG uploaded on the first try.

    “Three succeeded, then everything failed” felt like strong evidence for a limit. It was a coincidence of ordering, and we spent ten minutes acting on it.

    The same mistake twice

    Two of the three had the identical structure. We observed a sequence of outcomes, inferred a cause that fit the sequence, and acted on the inference — without once running the case we already knew worked down the same path at the same time.

    Two requests sent to the same endpoint in the same minute: a file known to upload successfully returns 200, while the failing figure returns 403, showing the block belongs to the file rather than to the sequence.
    The control request. Same endpoint, same credentials, same minute — and an answer that no amount of reasoning about the failing case could produce.

    A comparison baseline costs almost nothing. In the first incident it was one request to our other site. In the third it was one file we had already uploaded. Both were under a minute. Both would have removed the wrong answer immediately, and in the first case they would have saved us from weakening a security setting for no reason.

    The reason this bites harder with a WAF than with ordinary bugs is that a firewall gives you a block page instead of an error. There is no stack trace, no rule name, no line number. The absence of information invites you to fill the gap with a theory, and a theory that fits three data points feels like knowledge.

    What we actually changed

    Not the firewall. That was the other instinct worth resisting.

    The same firewall had blocked over two hundred real scans of the site that month. Switching it off to make our own pipeline work would have traded a known ongoing risk for a few minutes of convenience. The host’s WAF log lists every block with the rule that fired and an option to exclude that single rule. We excluded one rule — the SQL injection filter that cannot tell an English list from an attack — and left everything else running.

    For the image uploads we changed nothing on the server at all. The pipeline now converts to JPEG and retries when a PNG comes back 403.

    If you run the same setup

    Three things, in the order they save you time.

    Go to the WAF log before you form a theory. It names the rule. Everything we worked out by bisecting was sitting in that log the whole time, with the matched string highlighted.

    Send the known-good case down the same path, at the same moment. Not yesterday’s result, not the other environment’s result — the same path, now. If it also fails, your theory about what makes the failing case special is already wrong.

    Exclude rules, don’t disable the firewall. A single-rule exclusion is auditable and reversible. “We turned it off during the incident” is neither, and it tends to stay off.

    None of this is sophisticated. It is the discipline of confirming a difference before building on it, which is exactly the discipline that is hardest to hold when you are three hours in and the deploy is blocked.

    We have written up the gate system that sits in front of all of this. We have also written up what broke when we handed the work to scheduled agents. The pattern is consistent across all three write-ups: the failures were not caused by the automation being wrong. They were caused by us being confident about something we had not checked.


    Editorial note. Written and edited by the operator of this site. Every 403, timing, and retry count described here is from our own logs for the week of 2026-09-08. Where we describe a mistake, we made it.

    Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.