Appearance
Handle the bus's answers
A bus will refuse some of your messages and will sometimes be unavailable. What your product does next decides whether a newsroom loses data. This page makes that behaviour explicit.
One shape for every answer
json
{ "accepted": true, "message_id": "…", "message_type": "story.context", "family": "story-context", "tolerated": [ … ] }
{ "accepted": true, "duplicate": true, "message_id": "…" }
{ "accepted": false, "violations": [ { "source": "…", "rule": "…", "message": "…", "path": "/json/pointer" } ] }Authentication failures, rate limits and the firewall use the refusal shape too. The exceptions come from AWS in front of the bus, with a body of the form {"message":"…"}: on the routes signed with an AWS role (/v1/tenants/{t}/iam/…), 403 for a request that is unsigned, badly signed or signed with expired credentials; and 403 Missing Authentication Token for a path the API doesn't have.
Reading a refusal
json
{"accepted":false,"violations":[
{"source":"schema","rule":"envelope.format.uuid","path":"/message_id","message":"must be a valid uuid"},
{"source":"schema","rule":"story-context.required","path":"/payload","message":"must have required property 'story_id'"}
]}| Field | What it tells you |
|---|---|
| HTTP status | The class of problem, and whether a retry can help (below) |
source | Who says no: schema and conformance are SOM 1.0; sequence is SOM's snapshot-order rules, checked against the story's previous snapshot; policy is this bus's own rules. See Verdicts and refusals |
rule | A stable id. Branch on it, never on message. Each has a page in the rule catalogue with how to fix it |
path | A JSON Pointer into your message, where the bus can say where |
message | For people. It may change: don't parse it |
A message stops at the first stage that refuses it, but within SOM validation the bus reports every violation, not just the first. Don't assert on the number of violations: assert on the rule you expect.
Schema rules are named <schema>.<keyword>: the schema that failed (envelope, or the payload's family) and the JSON Schema keyword. envelope.format.uuid is a malformed id in the envelope; story-context.required is a missing member of a snapshot.
What to do with each answer
| Status | Meaning | Retry? | Your product should |
|---|---|---|---|
202 | Accepted and published for delivery | – | Carry on. Log anything under tolerated: warnings, not failures |
200 + duplicate | You already sent exactly this message, and it was published | – | Treat it as success. This is what makes retries safe |
400 | Not valid SOM, or not JSON | No | A bug in your product. Log rule and path, alert, don't resend it unchanged |
401 | Token missing, invalid or expired, or issued before a secret rotation | Once, with a new token | Refresh and retry once. If it fails again, it's configuration: stop and alert. (AWS signing failures are 403: refresh your AWS credentials) |
403 | Connection not active, the wrong workspace, an AWS role not yet verified, or a system_id or message type outside your grants | No | Configuration. Alert |
403 request.blocked | Refused by the bus's firewall | No | Check what your client sends; ask RND if it's a normal message |
409 on a sequence rule | This snapshot would break the story | Not this message | Your story state is behind or wrong. Rebuild a complete snapshot from current state, with the next sequence_number and a new message_id. A terminal story stays terminal: tell the user |
409 commit.contention | Another writer changed the story at the same moment | With a fresh snapshot | As above. If it keeps happening, two processes are writing one story |
409 story.not_owner | Your system doesn't own this story, and your workspace refuses such snapshots | No | Publish from the story's owner, or ask a workspace admin for a hand-off. The same rule under tolerated (the default) is a warning: the snapshot was accepted |
409 message_id.reused | This message_id was used for a different message | No | A bug: every new message needs a new message_id |
409 message.superseded | A retried snapshot was recorded but never published, and its story has accepted a newer snapshot since. It is not delivered | No | Nothing: the newer snapshot supersedes it. If the story needs a change, send a new snapshot from current state with a new message_id |
413 | Over 240,000 bytes | No | You're sending content, not references. Move media and long text out |
429 rate.limited or quota.exceeded | Over a rate limit, or your connection's daily allowance | Yes, with backoff | Retry the same message later, honouring Retry-After when present. Limits |
503 | bus.publish_failed or registry.unavailable: not published | Yes, with backoff | Retry with the same message_id and body |
503 message.in_flight | An earlier attempt of this message was recorded less than 30 seconds ago and isn't confirmed published yet. Nothing new was accepted | Yes, with backoff | Retry with the same message_id and body, for at least 30 seconds. See Retries |
Timeout, network error, other 5xx | Unknown whether it arrived | Yes, with backoff | Retry the same message_id and body. If it did arrive, you get 200 duplicate |
How to retry: Retries and idempotency.
Show it to people
Newsroom users shouldn't see "409". Decide what your UI or logs say for each class:
| Class | A user-facing meaning |
|---|---|
400 | "This story couldn't be shared with other systems: [field] is invalid." Point at path |
403 | "This system isn't allowed to publish that." For an administrator |
409 stale or terminal | "Someone else changed this story", or "This story was killed and can't be reopened" |
503, retries exhausted | "Other systems haven't received the latest version yet. Retrying." |
Checkpoint
- [ ] My client branches on status and
rule, never on the message text. - [ ] After a
409on a snapshot it rebuilds from current state and never resends the refused message. - [ ]
400,403and413alert and don't retry.
Next: Retries and idempotency.