Htmx Swaps That Never Arrive: Diagnosing a 200 Request With No DOM Update
Diagnostic guide for htmx when a request returns 200 but hx-target is not updated. Covers event logging, status and Content-Type checks, target resolution, and fixes tied to findings.
14 Sept 2025, 02:54 UTC

The problem you can see in devtools
A button or form with hx-get or hx-post fires, the Network tab shows a completed request, often with status 200, yet the element referenced by hx-target keeps its old markup or becomes blank. No visible change occurs and htmx:afterSwap is not observed.
The useful takeaway is that htmx can complete the fetch and still skip the DOM update. The swap decision happens after the response is read, based on status, headers, body, target resolution and swap configuration.
Recognizable condition
- Click or submit triggers an htmx request.
- Network shows a request finishing, sometimes with 200.
- The hx-target element does not change.
- htmx:beforeSwap fires but htmx:afterSwap does not, or the target appears empty.
Cause and diagnostic table
| Observed symptom | Likely cause | Why swap is skipped |
|---|---|---|
| Request finishes with 204, 3xx, 4xx or 5xx | Status not 200 | htmx skips swap by default for non-2xx responses |
| Status 200 but Content-Type is application/json or text/plain | Wrong Content-Type | Body is treated as non-HTML for the default swap |
| Status 200, text/html, body length 0 or whitespace | Empty fragment | Swap runs with empty content, target looks blank |
| Target selector resolves to nothing | Bad hx-target | No element to update, swap is a no-op |
| hx-swap="none" or invalid value | Swap disabled | Explicit instruction not to modify DOM |
| htmx not loaded or loaded twice | Init conflict | Handlers missing or duplicated, events not emitted |
Ordered checks
1. Confirm htmx is present and single
Run in the browser console on the page where the interaction occurs. No special permissions required.
typeof htmx !== 'undefined' && console.log(htmx.version)Expected check: a version string is printed once. If undefined, htmx is not loaded. If multiple script tags load htmx, remove duplicates.
2. Enable htmx event logging
htmx.logAll()Repeat the interaction and watch the console for htmx:beforeRequest, htmx:beforeSwap, htmx:afterSwap, and htmx:responseError. If beforeSwap fires but afterSwap does not, the swap was cancelled or the target was invalid.
Risk: logAll is verbose. Disable after diagnosis.
3. Inspect the request/response pair
In devtools Network, open the failing request.
- Record Status.
- Record Content-Type response header.
- Record response body length and first bytes.
Compare with a known-good htmx endpoint on the same page.
4. Verify target resolution
document.querySelector('button[hx-get], form[hx-post]')?.getAttribute('hx-target')Then test resolution:
document.querySelector('the-selector-from-hx-target')Expected check: exactly one element is returned. If null, the selector is wrong or the element is not yet in the DOM.
5. Temporarily force a safe swap
Add hx-swap="innerHTML" and hx-target="#known-id" to the triggering element for a test. Use a static id that exists in the page.
This isolates selector and swap configuration from server logic.
6. Check HX-* response headers and out-of-band swaps
Look for HX-Retarget, HX-Reswap, HX-Push-Url, and elements with hx-swap-oob in the response body. These can relocate content away from the expected target.
Fixes tied to findings
Non-200 status
Return 200 with an HTML fragment for normal swaps. If you intentionally use 4xx/5xx, add a responseError handler or an error extension instead of expecting a swap.
htmx.on('htmx:responseError', e => { /* handle */ })Risk: changing status may affect other clients that rely on error codes.
Wrong Content-Type
Emit text/html for fragments. Confirm on the wire, not only in framework return type.
Empty body
Return meaningful markup. If you need to clear the target, set HX-Reswap: true to force an empty swap explicitly.
Bad target
Correct the selector or ensure the target element exists before the request. For dynamically inserted targets, use hx-target="this" or a stable ancestor.
Swap misconfiguration
Choose innerHTML, outerHTML, afterbegin, or beforeend deliberately. Remove hx-swap="none" unless no DOM change is desired.
Init issues
Load htmx once before the first htmx-enabled element. Remove conflicting delegated listeners that call event.preventDefault() on the trigger.
Limitations and verification
Swapped innerHTML does not execute script tags. If a fix relies on scripted widgets, use htmx.process or an appropriate extension instead of assuming scripts run after a swap.
Browser fetch follows redirects, so a 3xx in the UI may hide a final non-200. Check the final status and body.
Practical verification:
- Add a temporary listener: htmx.on('htmx:afterSwap', e => console.log(e.detail.target, e.detail.xhr.status, e.detail.xhr.responseText.length))
- Serve a static HTML file with one hx-post button and a hard-coded text/html fragment to isolate server logic from client configuration.
Escalation criteria
Escalate when the request/response pair is already correct: 200, text/html, non-empty fragment, valid single target, default swap, and swaps still fail.
Also escalate when behavior differs between htmx 1.x and 2.x in ways not explained by documented defaults, when server-rendered markup is processed inconsistently across browsers, or when an extension such as idiomorph, preload, or response-targets appears to override swap semantics.
Pin a version and read its release notes before generalizing. htmx defaults are version-sensitive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.