What a 200 from the WordPress REST API does not tell you

Posted by:

|

On:

|

On 14 August a retry agent was working through a batch edit against our own live site over the WordPress REST API. On one published article it wrote its own escaping test string into the content field. The whole body was replaced by a short diagnostic fragment the agent had been using to check its own quoting. The POST returned 200. The JSON validated. The agent finished the group and reported that it had completed successfully.

We got the article back only because an unrelated task earlier that week had happened to leave a copy of the body on disk. Nothing in the procedure put it there. We were lucky exactly once.

What we thought we were verifying

Our checks at the time were a status code and a schema validation, and we treated the pair as confirmation that the edit had landed as intended. Both checks are real and both go green when things go well. They just answer a different question than the one that mattered.

A status code reports transport: the request arrived, the server accepted it, the handler did not throw. A schema validator reports syntax: the payload was shaped the way the endpoint expects. Neither has any opinion about whether the bytes in the body are the bytes you meant to send. The agent’s own summary was worse than useless — it described the agent’s intent from inside the agent’s own context, the one vantage point from which this error is invisible.

Why the WordPress REST API makes this particular write dangerous

The mechanism is not exotic. The content field is destructive: it replaces, it does not add. No partial writes, no merge, no append mode. Whatever you transmit becomes the entire stored body, and a short string is as valid as a long one. An escaping test fragment is a perfectly legal article as far as the endpoint is concerned.

Stack that against what a status field can express. A 200 means the write completed. It cannot mean what is now stored is what you sent, because the endpoint has no access to your intent — it only has the payload. The one distinction you need, between “saved” and “saved as something other than what I sent,” is precisely the one no status field can draw. Asking for it is a category error, not a missing feature.

There is a second edge on the same blade. The WordPress REST API can hand you a rendered representation or a raw one, and the rendered form has already passed through display filters. It is not round-trippable: read it, edit it, write it back, and the stored markup is corrupted while every check you own still reports success.

Parallel agents make it strictly worse

Run several agents against the same post and this stops being an accident of one bad payload. Each reads, builds its version of the body, and writes. There is no compare-and-set, so the last write wins and every agent receives its own 200. A body overwritten by a sibling seconds ago is indistinguishable, from inside any one agent, from a body it wrote itself. The batch reports full success and the article is whatever finished last.

Not every write through the WordPress REST API is the same risk

The corollary matters as much as the warning. A write that transmits only scalar fields — a title, a slug, a status, an excerpt — and never transmits content at all is in a different risk class entirely. A field you never send cannot be replaced by what you did not send.

Wrapping a one-field change in the full body-write discipline is not extra safety, it is cost with nothing on the other side of it. We measured it: applying the heavy loop to a single-field change consumed 99,749 tokens against a 59,104-token baseline that reached the identical verified result. Ceremony beyond the risk is not caution. So the first decision in any write is which of the two you are doing — merely expensive to get wrong in one direction, destructive in the other.

What follows for anyone building this

What matters here is not a checklist but an ordering. When the previous value is the only copy that exists, reading it first is not caution — it is part of the write operation. A write that cannot produce the prior state on demand is not a safe write that went well; it is an unsafe write that has not failed yet.

The rest follows. Verification runs against stored bytes — lengths, opening and closing characters, the structural blocks you intended, whether any appears twice — and never against the platform’s success flag. The read-back has to come from something that did not perform the write, because a process confirming its own work is confirming its intent, not the result. A body far below a plausible length floor is wiped, not short. And the window in which the original is recoverable closes at the next write, which is why restoring comes before investigating.

The runnable procedure, with its guards and the checkpoint that lets a third party re-derive the verdict, lives in the member library.

How we know

Grounded in: our own batch incident on 2026-08-14, when a retry agent wrote its escaping test string into the live content field of a published article, and the controlled comparison run on 2026-08-15. Verified: the expected stored body reconstructed locally from the saved original measured 12,282 bytes and the live read-back measured 12,282 bytes; the modification timestamp advanced and the revision count moved 2 to 3. No green tick was consulted. The runnable procedure lives in the member library.

Leave a Reply

Your email address will not be published. Required fields are marked *