On 21 October Buffer refused one of the desk's posts before writing any of it. Buffer is the scheduler the desk queues its social posts through, and the morning's Threads thread went to its GraphQL createPost mutation with the image in the shape that had worked for weeks, assets: {images: [{url}]}. The error named the AssetInput type and suggested image. The desk rebuilt the field as a list, [{"image": {"url": "..."}}], sent it again, and the thread scheduled on the first try.
The whole incident cost one round trip. It was the second time in five weeks that Buffer had changed what it would accept, and both times the change arrived as an error.
Five weeks, two refusals
On 15 September the football desk's thread failed with an InvalidInputError: Threads, Buffer said, did not support the thread post type, and the valid types were post and ghost_post. The day before, a payload carrying type: "thread" had gone through, and reading that post back showed type: "post". Buffer had been translating the old value for the desk without saying so, and then it stopped. A failed create writes nothing, so the resend with type: "post" was safe, and the desk lost a few minutes.
Neither refusal needed anyone to notice anything. The run stopped at the step that was wrong, with the name of the field in the error.
The fields it took and threw away
The expensive failures were the quiet ones. On 3 August the desk read back three Instagram posts it had created through Buffer's simplified create_post tool with a first comment set. All three came back firstComment: null. The same tool had dropped the image on four Threads threads in a row, on 30 and 31 July and 1 and 3 August, and each one published as text. Nothing errored. For weeks the first-comment routine looked as if it was running, and almost no post had carried one.
The same thing happened three more ways. A .webp portrait attached to a Threads post was accepted, then read back as assets: [], and the thread went out without it. An editPost asking for schedulingType: automatic returned success twice on 4 August and left the stored value where it was. On 17 August one run created two LinkedIn posts eleven seconds apart without setting saveToDraft. One sent on 18 August. The other sat as a draft through its 20 August slot and never published, and nothing reported it.
An old rule wrote the silent version
Swallowing bad input has a long pedigree. RFC 1958, the IAB's statement of the Internet's architectural principles, gave the robustness principle as strict when sending and tolerant when receiving, and told implementers that when in doubt they should “discard faulty input silently” [1]. That is a fair description of what happened to the desk's first comments.
RFC 9413, published in June 2023 by Martin Thomson and David Schinazi, makes the case against that habit. Its section on harmful consequences describes how tolerated faults become a de facto standard that every later implementation has to copy, and its remedy, which it calls virtuous intolerance, is to generate fatal errors for unspecified conditions so that faults get attention and new implementations get fast, strong feedback [1].
GraphQL builds the intolerant version in. The October 2021 specification requires every field in an input object to be defined on that object's expected type [2], and a request that fails validation must fail without executing [3]. The 21 October error was that rule at work: images was no longer a field, so the mutation died before it could write half a post.
The silent drops and the loud refusals came from the same vendor, by different routes. The simplified tool advertised firstComment in its schema and did not pass it on; the GraphQL createPost mutation, tried on 4 August, carried it through on four posts in a row. The desk moved its writes to the mutation and has kept them there.
No version to pin
A REST API that changes shape usually earns a new version number, and a client can stay on the old one for a while. The GraphQL project's own guidance on schema design takes a firm line against versioning and expects a schema to evolve in place, with new types and fields added to the one that already exists [4]. The schema the desk calls today is the only one there is.
That moves the risk onto anything saved. After 21 October the staged vars.json files from earlier runs carried the dead images shape, and the desk's standing note now says never to copy one verbatim. A replayed payload is a letter addressed to a house that has since been rebuilt.
What the desk checks now, and one check to add
Writes go through the route that validates, which here is the GraphQL mutation and never the simplified tool. Every field that decides whether a post sends is set explicitly on every create: mode, schedulingType, needsApproval and saveToDraft. Then a separate query reads the post back and requires the fields the run depends on: assets not empty, firstComment present where one was sent, status reading scheduled.
The check worth adding is cheaper than a failed run. Before any old payload is replayed, ask the schema what the input type takes today. Introspection is part of the specification, and for an input object type the __Type record returns its inputFields [5].
query {
__type(name: "AssetInput") {
inputFields { name }
}
}
If the answer lists image and not images, the saved file is stale before it leaves the machine, and the run can rebuild it instead of finding out from the API.
The LinkedIn draft from 17 August is still unexplained. The same run created it eleven seconds after a post that sent, and nothing in the desk's record says why one went out and the other did not.
Sources
- RFC Editor, RFC 9413: Maintaining Robust Protocols, M. Thomson and D. Schinazi, June 2023, sections 2, 4 and 5.1.
- GraphQL Specification, October 2021 edition, section 5.6.2, Input Object Field Names.
- GraphQL Specification, October 2021 edition, section 6.1.1, Validating Requests.
- GraphQL Foundation, Schema Design: Versioning.
- GraphQL Specification, October 2021 edition, section 4.2.2, The __Type Type.