Skip to content
Settings

ENTITY_MODIFIED — If-Match precondition failed; entity changed since last read

cyoda-go version 0.7.1

ENTITY_MODIFIED — an If-Match-guarded entity update was rejected because the supplied transaction-ID no longer matches the entity’s current version.

HTTP: 412 Precondition Failed. Retryable: no.

When an entity update request carries an If-Match header, the server requires the supplied transaction ID to equal the entity’s current meta.transactionId. A mismatch means another writer has updated the entity since the caller’s last read. The optimistic-concurrency guard rejects the update rather than silently overwrite.

The entityId property in the problem-detail body identifies the conflicting entity.

Not retryable in the protocol sense — replaying the same payload with the same If-Match value will fail again.

  1. Re-read the entity: GET /api/entity/{entityId}. The response envelope’s meta.transactionId is the entity’s current version.

  2. Reconcile your change against the current state. Whatever the concurrent writer changed is now the baseline; merge or override it intentionally rather than blindly replaying your previous payload.

  3. Re-submit the update with the fresh If-Match:

    curl -X PUT \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -H "If-Match: <meta.transactionId from step 1>" \
    -d '<reconciled payload>' \
    "http://localhost:8080/api/entity/JSON/{entityId}[/{transition}]"

A second 412 ENTITY_MODIFIED on retry means another writer raced you again. Either accept the loss (drop your change), back off and retry the read-reconcile-write loop with jitter, or escalate to a coarser locking strategy (lock the model, or coordinate writers out of band) — naive looping will livelock under contention.

RELATIONSHIP TO TRANSACTION-LEVEL CONFLICT DETECTION

Section titled “RELATIONSHIP TO TRANSACTION-LEVEL CONFLICT DETECTION”

If-Match is not the only conflict guard on entity updates; it is an additional, narrower one. Every PUT runs inside a SERIALIZABLE transaction with first-committer-wins (SI+FCW) validation at commit time. The handler reads the entity inside the transaction, the read is recorded in the read-set, and any concurrent committer who changes the same entity between this PUT’s transaction start and its commit is detected — the commit fails with 409 CONFLICT and retryable: true (the RetryableConflict path). PostgreSQL’s own SQLSTATE 40001 detection covers write-write races equivalently. So a PUT cannot silently overwrite a concurrent writer that committed during its own transaction window, regardless of whether If-Match was supplied.

What If-Match adds is a precondition tied to the caller’s earlier read in a different HTTP request, before this PUT’s transaction even began. Two concrete scenarios:

Cross-request race (caller GET-then-PUT, another writer commits in between). Caller GETs at t0 and observes transactionId T0. Another writer commits a change at t1. Caller submits the PUT at t2, with t0 < t1 < t2. With If-Match: T0, the PUT fails 412 ENTITY_MODIFIED because the entity’s current transactionId is no longer T0. Without If-Match, the PUT’s own intra-transaction GET at t2 already sees the writer’s change as the current baseline; the PUT proceeds and applies the caller’s payload on top of the writer’s change — the caller never sees that they overwrote a state they hadn’t read.

Overlapping-transaction race (two PUTs starting from the same baseline). Two PUTs both begin a transaction and read the same entity version. With If-Match, the loser fails fast at write-time as 412 ENTITY_MODIFIED via CompareAndSave. Without If-Match, the loser fails at commit-time as 409 CONFLICT with retryable: true via SI+FCW read-set validation.

So omitting If-Match does not turn off all concurrency control — but it does silence the cross-request precondition. Use it when reconciling against the live state at PUT-time is what you actually want; supply it when you specifically need to detect that the entity has changed since your last read and refuse to clobber.

  • errors
  • errors.CONFLICT
  • errors.IDEMPOTENCY_CONFLICT
  • errors.EPOCH_MISMATCH
  • cyoda help errors — Every error response from the Cyoda REST API carries a structured errorCode in the properties object. Multiple codes may share the same HTTP status. Programmatic handling keys on errorCode, not HTTP status.
  • cyoda help errors CONFLICT — The server detected that the entity was modified by another writer between the time it was read and the time the current write was committed. Normal outcome under concurrent load.
  • cyoda help errors IDEMPOTENCY_CONFLICT — The idempotency key is supplied via the Idempotency-Key HTTP header on collection create and update requests. See crud for the request shape.
  • cyoda help errors EPOCH_MISMATCH — Shard ownership is tracked by an epoch counter that increments whenever the cluster re-partitions. A write is rejected with this error when the writing node’s cached epoch is stale — another node has since taken ownership of the shard. This prevents split-brain writes.