When an htmx swap paints the wrong thing — a whole page inside a<div>, a
section that goes blank, an out-of-band update that does nothing, a
Suspense skeleton that never
resolves — work through this page top to bottom: run the contract checker, turn
on debug mode, then read the symptom table. Most swap bugs trace to one wrong
return type.
Diagnose in order
- 1
Start with the contract
Before guessing from screenshots, run the contract checker:
chirp check app:appFor CI or release branches, fail the build on warnings too:
chirp check app:app --warnings-as-errorschirp checkcatches the failures that most often turn into blank or wrong DOM: missing template blocks, route-name collisions, OOB registrations that no layout satisfies, route-directory metadata mismatches, reactive block typos, form-contract gaps, and known htmx footguns. For the full list of categories and their severities, see contract categories. - 2
Enable debug mode
Use the development CLI when possible:
chirp dev app:appOr configure the app directly:
from chirp import App, AppConfig app = App(AppConfig(debug=True))Debug mode enables richer errors, template reloads, debug headers, the fragment validator (
debug_fragment_validator, on by default), and the browser DevTools overlay. - 3
Open browser DevTools
Open the app and press
Ctrl+Shift+Dto open Chirp DevTools.For browser-capable agents, export records straight from the page:
window.ChirpHtmxDebug.help() window.ChirpHtmxDebug.exportRecordsJson()The exported records include htmx requests, errors, SSE connections and events, View Transition events, render plans, and Swap Doctor diagnostics. Reach for this when the visual symptom is vague but the request, target, and render intent should be precise.
Symptom table
| Symptom | Likely cause | First check |
|---|---|---|
A whole page appears inside a<div> |
Handler returnedTemplate(...)for an htmx request |
UsePage(...) or Fragment(...); the debug fragment validator should warn |
| A section goes blank after a swap | Targeted block is missing or rendered empty | chirp check; verify the Fragment or Pageblock name |
| OOB update does nothing | hx-swap-oobtarget id does not exist in the current layout |
Check the OOB registry and layout block ids |
| Suspense skeleton never resolves | Deferred block was not discovered or mapped to the wrong target | Checkdefer_blocks, defer_map, and block dependencies |
| Empty list looks like loading | Template used{% if items %}for a deferred value |
Use{% if items is deferred %}before testing resolved values |
| SSE stream stops after one bad event | Error boundary widened beyond one event | Check theEventStreamgenerator and fragment render errors |
| SSE events arrive but DOM never updates | sse-swapevent name mismatch or swap on the connect element |
Runchirp check for sse_crossref / sse_self_swap; use assert_sse_wired in tests; put sse-swapon a child sink |
| Boosted link reloads the page | Link crosses shell boundaries or boost is disabled | CheckHX-Redirect, shell layout domains, and hx-boostinheritance |
| Duplicate shell actions or badges appear | OOB region rendered inline and out-of-band | Register the region and keep the DOM id in one owner |
Keep the return type honest
Most swap bugs reduce to the wrong return type:
| Need | Return |
|---|---|
| Full page only | Template(...) |
| Full page for browsers, fragment for htmx | Page(...) |
| One named block | Fragment(...) |
| Main fragment plus extra regions | OOB(...) |
| Initial shell plus deferred blocks | Suspense(...) |
| Post-load long-lived updates | EventStream(...) |
If the response shape is unclear, start at Return Values.
Advanced: debug headers
Whendebug=True, Chirp exposes request and route context through response
headers:
X-Chirp-Route-KindX-Chirp-Route-FilesX-Chirp-Route-MetaX-Chirp-Route-SectionX-Chirp-Context-ChainX-Chirp-Shell-Context
Inspect these when a filesystem route, section, shell mode, or layout chain does not match the route you thought was serving the request.
Advanced: template-context contracts
Kida-powered template diagnostics report under their own categories:
| Category | Meaning | Typical fix |
|---|---|---|
component |
A local{% def %}call has unknown, missing, duplicate, or literal type-mismatched arguments |
Fix the call site or the component signature |
template_context |
An opt-in dotted context contract does not cover what the template reads | Add the missing path toprovided, move it to optional, or stop reading it |
template_escape |
A template deliberately trusts markup, such as| safe, and should document the trust boundary |
Addsafe(reason="...")or remove the trust override |
template_privacy |
A template reads sensitive-looking data such as tokens, secrets, or password paths | Confirm the value belongs in rendered output or remove it |
For dotted context checks, register template contracts through the contract check-data channel:
app.set_contract_check_data(
"template_context_contracts",
{
"page.html": {
"provided": {"page.title", "user.name"},
"optional": {"flash.message"},
}
},
)