htmx hx-post retry configuration for non-idempotent form submissions
0 reputation · 15 May 2022, 04:21 UTC
0 reputation · 15 May 2022, 04:21 UTC
I have a form using hx-post to submit an order creation request. htmx issues the AJAX request directly from the markup, and the server performs a write that is not naturally idempotent (it creates a new row per request). I want a retry story for flaky networks without risking duplicate writes.
As far as I can tell, htmx itself does not document a built-in automatic retry policy for failed requests, so any retry would be triggered via events such as htmx:responseError or htmx:sendError, or by re-triggering the element. The open question is how to pair that client-side retry with a server-side deduplication mechanism (for example an idempotency key sent as a request header) using only documented htmx configuration such as hx-headers or event listeners, without the key changing between the original attempt and the retry. Assume htmx 1.9.x and a standard JSON-over-POST backend.
hx-post request so a manually re-triggered retry reuses the same key?htmx:beforeRequest to prevent double-clicks, and does it interfere with an intentional retry?htmx:sendError versus htmx:responseError, given that a 5xx may mean the write already happened?35275 reputation · 15 May 2022, 05:48 UTC
1. Stable idempotency key: generate the key once per page load (or first submit) in JavaScript, store it in a global variable or a hidden input, and add it to every hx-post with hx-headers or via an htmx:configRequest listener. The same key is sent on every retry because it is not re‑generated.
2. Disabling the trigger: use the htmx:beforeRequest event to call elt.setAttribute("disabled", "disabled") or elt.disabled = true. This prevents double‑clicks. It does not block an intentional retry because a retry can first re‑enable the element (e.g. elt.disabled = false) or trigger the request programmatically with htmx.trigger(elt, "submit"), which bypasses the disabled state.
3. When to retry: retry on htmx:sendError (network failure, no response) and, if the server guarantees idempotency, also on htmx:responseError for 5xx responses. Do not retry 4xx responses; they indicate a client error that will not succeed on repeat.
Generate the key (once per page load):
const idempotencyKey = crypto.randomUUID();
// Store in a hidden input for easy access by htmx
const hidden = document.createElement("input");
hidden.type = "hidden";
hidden.name = "Idempotency-Key";
hidden.value = idempotencyKey;
document.querySelector("form").appendChild(hidden);
Attach the key to every request via hx-headers or htmx:configRequest:
<form hx-post="/orders" hx-headers='{"Idempotency-Key": "${idempotencyKey}"}'>
…
</form>
Because the variable is evaluated only once, the header stays the same on retries.
Disable the submit button to avoid double‑clicks:
document.querySelector("form").addEventListener("htmx:beforeRequest", (e) => {
e.target.querySelector("button[type=submit]").disabled = true;
});
Retry logic – listen for htmx:sendError (or htmx:responseError if you trust idempotency on 5xx):
const maxRetries = 3;
let attempt = 0;
function retry(elt) {
if (attempt >= maxRetries) return;
attempt++;
// Optionally re‑enable the button for a retry
elt.querySelector("button[type=submit]").disabled = false;
// Back‑off: 500ms, 1000ms, 2000ms …
setTimeout(() => htmx.trigger(elt, "submit"), 500 * Math.pow(2, attempt-1));
}
document.querySelector("form").addEventListener("htmx:sendError", (e) => retry(e.target));
Server‑side deduplication (must be implemented separately): store the idempotency key in a unique column. On a duplicate key, return the original response instead of performing the write again.
htmx:beforeRequest; re‑enable it before a programmatic retry.htmx:sendError for network failures; 5xx can be retried only if the server guarantees idempotency.Does your backend already enforce idempotency (unique constraint on the key) or will you need to add that logic?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.