Apply, Conflicts & Rollback

Nothing in HAProxy UI reaches a running HAProxy until you apply. This page covers what happens in between, and what to do when it goes wrong.

Versioned writes

Every apply is scoped to the configuration version the draft was based on. If the live configuration has moved since you opened it, the write is rejected rather than silently overwriting somebody else’s change. You get a conflict dialog offering Reload & re-diff, not a generic error.

Transactions

The apply path opens a Dataplane API transaction, scopes each per-resource operation to that transaction id, then commits it. This is the correct v3 shape and it matters more than it sounds.

Scope operations to the transaction, not to a version. Opening a transaction advances the API’s tracked configuration version immediately. If per-resource operations are sent with ?version= using a version captured before the transaction was opened, every operation returns 409 version mismatch and the transaction aborts — every time, with no useful error. Scoping with ?transaction_id= and committing is what makes an apply atomic.

The commit bumps the configuration version by exactly one.

Partial-write hazard

Dataplane v3 PUT replaces the resource. Any field not present in the request body is deleted. This has a sharp edge worth understanding: sending a subset body to update one field silently removes every other field on that resource.

HAProxy UI sends the full modelled representation of each resource rather than a delta, so unrelated fields survive an edit that only changed one value. If you add a field the app does not model, it is carried in rawLines and re-sent — but verify against a live instance if you are introducing a directive the app has never seen.

Graceful reload

A successful commit is followed by a graceful reload through the Dataplane API’s reload endpoint. The reload command runs haproxy -c before signalling the master process, so a config that would fail validation does not take down a running instance.

Validation before send

Drafts are validated locally before anything is transmitted. Errors are typed and attributed to a node and field — duplicate names, a frontend with no bind line, per-server constraint violations. Fixing these locally is faster and safer than discovering them as a rejected transaction.

Conflict handling

  • Reload banner — surfaces when the Dataplane version has moved and the canvas is stale.

  • Live conflict dialog — offers reload-and-rediff rather than a blind retry.

  • Reject-confirm dialog — an explicit confirmation step before clobbering somebody else’s pending change.

Snapshots and rollback

Configuration snapshots are stored per instance and browsable from the version-history sheet. Roll back to any prior snapshot; the rollback is itself an ordinary apply, so it goes through the same validation, diff and conflict path as any other change.

Pending changes

Every unsaved edit is listed in the pending-changes bar. The working draft persists to localStorage keyed by instance id, so it survives a relaunch, and is cleared on successful apply or explicit discard. Discarding is not reversible — the draft is not a snapshot.

Runtime view

  • Stats dashboard — process info from show info, plus a per-backend × per-server health matrix, auto-refreshing every 5 seconds.

  • Sync-state badge — for GitOps instances, surfaces clean, local-changes, behind or diverged from git status, refetched every 60 seconds.

Troubleshooting

Apply fails with 409 version mismatch

The live configuration moved. Reload and re-diff, then re-apply. If this happens on every apply rather than occasionally, the transaction is not being scoped correctly — per-resource calls must carry ?transaction_id=, not ?version=.

Apply fails with a field-specific validation error

Read the field name in the message. For balance errors see Balance Algorithms — the most common cause by a wide margin is a missing hash expression or hash type.

A field disappeared from a backend

Either the directive is not modelled and was dropped, or a partial write replaced the resource. Check the raw-payload panel and the snapshot history.

Browser build cannot reach the Dataplane (CORS errors)

Use the Vite proxy: set the instance URL to /dataplane and point PUBLIC_DATAPLANE_BASE_URL at the same relative path. Direct absolute URLs require cors_allowed_origins on the Dataplane side. The desktop build is unaffected — its requests originate in Rust.

Keychain writes fail on Linux

A Secret Service must be running. On a headless box, dbus-launch --exit-with-session gnome-keyring-daemon is the usual fix.

Verify the apply path end to end

Two runbooks ship with the source and cover exactly these paths against a real Dataplane rather than the mock:

  • docs/verify-live-apply.md — reproduces and confirms the transaction-scoping and partial-write behaviour, with exact curl sequences.

  • docs/verify-desktop.md — proves the desktop build routes Dataplane traffic through Rust with no webview fetch(), using devtools. Requires a GUI window, so it cannot run in CI.


Was this article helpful?