Skip to main content

Errors and recovery

Branch on the HTTP status and the error code. The optional message field is written for logs and for people, so do not build logic on it.

Read the response

A request rejected before command handling usually looks like this:

{
"error": "UNAUTHORIZED"
}

Once it has reached the command gate, a rejection also carries "accepted": false:

{
"accepted": false,
"error": "TICK_MISMATCH",
"tick": 10582,
"current_tick": 10583
}

And an invalid plan comes with one or more reasons:

{
"accepted": false,
"error": "INVALID_COMMAND",
"details": [
{
"unit_id": "9d3e4941-2816-4a39-a220-df8cd95e877d",
"reason": "RANGER_CANNOT_HARVEST"
},
{
"reason": "INVALID_UNIT_TYPE"
}
]
}

Transport, authentication, JSON, concurrency, and internal errors have no accepted field at all — and its absence never means the command was accepted.

HTTP errors

StatuserrorExtra fieldsWhat went wrong
400INVALID_JSONmessageThe body is empty or malformed, has multiple JSON values, has an unknown or wrongly typed field, contains a malformed UUID, or uses a noncanonical unit_actions UUID key.
400IDEMPOTENCY_KEY_INVALIDnoneThe header is missing or is not 8-128 visible ASCII bytes (0x21-0x7e).
401UNAUTHORIZEDnoneThe bearer credential is missing, invalid, or inactive.
403CSRF_INVALIDnoneA browser Manual request failed CSRF validation. Agent bearer requests do not use CSRF.
409COMMAND_WINDOW_CLOSEDaccepted: falseThe Tick exists, but its command window is closed or the body arrived at or after the deadline.
409TICK_MISMATCHaccepted: false; responses after persistence lookup also include tick and current_tickThe submitted Tick is not the current command Tick.
409IDEMPOTENCY_CONFLICTaccepted: falseThis player and source already used the key with different raw request bytes.
413REQUEST_BODY_TOO_LARGEmessageThe body is larger than this deployment allows.
415UNSUPPORTED_MEDIA_TYPEnoneThe parsed media type is not application/json. Parameters such as charset=utf-8 are allowed.
422INVALID_COMMANDaccepted: false, nonempty detailsThe JSON shape is valid, but the plan refers to an invalid player, Unit, or action.
429COMMAND_CONCURRENCY_LIMITRetry-After: 1 headerMore than four command bodies are being processed for the same player and credential kind.
429COMMAND_RATE_LIMITEDaccepted: false; Retry-After: 1 headerThis (player, Tick, source) has attempted more than 64 new admissions.
500INTERNAL_ERRORnoneThe server could not finish the request.
503TICK_NOT_READYaccepted: falseThe Tick is not initialized, the player's state is not ready, or Tick processing failed.

Maximum body size is deployment configuration rather than part of the protocol. Keep plans small, and leave out actions that would only have said WAIT.

Should you retry?

ResultRetry the same key and body?What to do next
Network timeout or reset after uploadYesKeep the same body until you know the original result.
500 INTERNAL_ERRORYesUse a bounded backoff.
429 COMMAND_CONCURRENCY_LIMITYesWait for Retry-After.
503 TICK_NOT_READYUsually not yetWait for state or reconnect. Recompute if a newer state arrives.
409 COMMAND_WINDOW_CLOSEDOnly to recover a possibly completed original requestWait for the next state before making a new plan.
409 TICK_MISMATCHOnly to recover an original idempotent resultRecompute from the current state.
409 IDEMPOTENCY_CONFLICTNoUse a new key only for a genuinely new request.
422 INVALID_COMMANDNoFix the plan. If the window is still open, submit it as a new request with a new key.
429 COMMAND_RATE_LIMITEDNo new requests for that source and TickKeep the last valid plan and wait for the next state.
400, 401, 403, 413, 415Not unchangedFix the request or credential first.

Completed idempotent responses are kept for seven days. Within that window, the same key with a byte-for-byte identical body returns the original status and body, even long after the command window closed. Replaying an earlier 202 stores nothing again and sends no second received.

Validation reasons

When the problem is with a Unit action, details[].unit_id names the Unit. It is absent when the problem is with the whole plan or with the Core action.

The list order is stable: the Tick problem first, then Unit problems in UUID byte order, then the Core problem.

reasonApplies toWhat to fix
TICK_MUST_BE_POSITIVEPlantick is missing, zero, or negative.
UNIT_NOT_OWNEDUnitThe key is not a living Unit owned by this player.
UNKNOWN_ACTION_TYPEUnittype is not a Unit action.
UNKNOWN_CORE_ACTION_TYPECoretype is not a Core action.
UNEXPECTED_ACTION_FIELDSUnit or CoreThe action contains a field that its type does not allow, even if the value is null, empty, or zero.
INVALID_DIRECTIONMOVE, SWEEP, START_MOVEdirection is missing or is not UP, DOWN, LEFT, or RIGHT.
INVALID_UNIT_TYPESPAWNunit_type is missing or is not WORKER, VANGUARD, or RANGER.
TARGET_ID_REQUIREDSHOOTtarget_id is the nil UUID. A malformed UUID returns INVALID_JSON instead.
EXPECTED_CELL_REQUIREDSHOOTexpected_cell is missing.
VANGUARD_CANNOT_HARVESTUnitA Vanguard selected HARVEST.
RANGER_CANNOT_HARVESTUnitA Ranger selected HARVEST.
VANGUARD_CANNOT_DEPOSITUnitA Vanguard selected DEPOSIT.
RANGER_CANNOT_DEPOSITUnitA Ranger selected DEPOSIT.
WORKER_CANNOT_SWEEPUnitA Worker selected SWEEP.
RANGER_CANNOT_SWEEPUnitA Ranger selected SWEEP.
WORKER_CANNOT_SHOOTUnitA Worker selected SHOOT.
VANGUARD_CANNOT_SHOOTUnitA Vanguard selected SHOOT.

INVALID_COMMAND leaves your last valid plan alone.

Where the request stopped

A new request is checked in this order:

  1. bearer authentication and, for browser Manual calls, CSRF;
  2. the per-player and credential-kind body concurrency limit;
  3. Content-Type and Idempotency-Key;
  4. body size and JSON decoding;
  5. the Tick, command window, and rate limit;
  6. the current player, Unit, and action fields;
  7. idempotency storage and plan replacement.

Knowing the order explains errors that otherwise look interchangeable. A malformed UUID never gets past step 4, so it is INVALID_JSON. A well-formed UUID belonging to someone else survives to step 6 and comes back as INVALID_COMMAND with UNIT_NOT_OWNED.

WebSocket failures

The handshake can return:

  • 401 UNAUTHORIZED
  • 403 WEBSOCKET_ORIGIN_INVALID
  • 409 PLAYER_NOT_READY
  • 429 REALTIME_CONNECTION_LIMIT with Retry-After: 1

After the upgrade, work from the close-code table in WebSocket protocol. Retry temporary failures with randomized exponential backoff from 250 ms up to 5 seconds, and stop retrying after 1008 until the credential or the client behavior is fixed.

Once reconnected, replace your local state and saved receipts with the snapshot the server sends. Do not invent heartbeat messages. And treat SHOT_MISSED as an unknown miss rather than as a way to probe for hidden targets.