Skip to content

Safe writes ​

A caller that is a program rather than a person guesses, retries and races. Three features of the management API, available to operators and automation tokens alike, make that safe: dry runs, idempotent creates and optimistic concurrency. All three are opt in per request, and none changes the behaviour of a request that does not use them.

Dry runs ​

Add dryRun=true to the query string of a supported write and the server does everything it would do for the real write except persist: it binds the body, runs the same checks and validations, and answers 200 with what would have changed. Nothing is written, no worker is notified, and the audit trail records event 7091 (a dry run) instead of the change the route is for.

POST /api/v1/adm/users?dryRun=true

The answer carries the header X-Dry-Run: true and a body of this shape:

json
{
  "valid": true,
  "action": "update",
  "objectType": "user",
  "objectId": "alice",
  "errors": [],
  "diff": {
    "added": {},
    "changed": { "home.vfsId": { "from": "vfs1", "to": "vfs2" } },
    "removed": {}
  },
  "dependents": [],
  "message": "Dry run completed; nothing was persisted",
  "messageCode": "webrest.dryRun.completed"
}

valid false comes with the same coded errors the real write would have answered with. diff lists the fields that would change, as dotted paths into the stored document; arrays are compared as a whole. Secrets are never shown: passwords and encrypted values are replaced by a short fingerprint on both sides before the comparison, so the difference can say that a secret changes without saying what it is. dependents lists what else the write would touch, for example the app passwords and passkeys a user delete takes with it, or the tokens an Admin delete revokes.

Dry runs are supported on: users, VFS, scripts and secrets (Admin API), the site configuration (PUT and PATCH /adm/srvconfig, PATCH /adm/srvconfig/{mode}), the global configuration, virtual sites and their Admin accounts (SuperAdmin API). On every other route the flag is refused with 400 and webrest.request.dryRunNotSupported, so a client can never have its flag silently ignored by a route that then persists. A read only token may not dry run a write: the token scope is checked first.

Idempotent creates ​

A POST that creates an object may carry an Idempotency-Key header of up to 128 printable characters, chosen by the client (a UUID is a good choice). The server remembers the outcome for 24 hours. A retry with the same key, from the same actor, gets the original answer back, status and body included, with the header Idempotency-Replayed: true, and creates nothing. The same key sent with a different request body is refused with 422 and webrest.request.idempotencyKeyMismatch; a second request while the first is still running is refused with 409 and webrest.request.idempotencyInFlight. A replayed answer is not audited again; the original was.

Supported: POST /adm/users, POST /adm/users/{id}/clone, POST /adm/vfs, POST /adm/script, POST /adm/secrets, POST /sa/vsites, POST /sa/vsites/{id}/adm. Routes whose answer carries a secret (tokens, generated keys, recovery codes) never support it, because a replayed secret is a stored secret; on those and on every other route the header is ignored. The remembered outcomes replicate across a cluster, so a retry that reaches another node behind a load balancer is still recognized.

Optimistic concurrency ​

A GET of a single user, VFS, script, Admin, virtual site, site configuration or global configuration answers with an ETag header: a weak validator (W/"...") computed from the stored document. A PUT, PATCH or DELETE of the same object may carry If-Match with that tag. When the stored document no longer matches, because someone changed it in the meantime, the write is refused with 412 and webrest.request.preconditionFailed, and nothing is written. Without If-Match the write proceeds as it always did.

If-Match: * passes whenever the object exists. Several tags may be listed, separated by commas. On a route that does not support the header the request is refused with 400 and webrest.request.ifMatchNotSupported, never silently honoured.

NOTE

The tag covers the stored document, so any save of the object changes it, even a save that wrote the same values. Read again after every write.

Order of checks ​

For a request that uses all of this, the server checks, in order: the credential and its revocation, the token's scope (forbidden routes, read only, service actions), the dry run flag, If-Match, the idempotency key, the permission the route requires, and finally the request itself. A refusal names the first check that failed.