Errores y códigos de estado

Por: Artiko
htmxhtmx4hx-statuserroreshttpheaders

Errores y códigos de estado

htmx 4 cambió el comportamiento por defecto ante errores, y añadió un atributo dedicado para controlarlo con precisión. Es uno de los cambios que más simplifica el código real.

El cambio: los errores ahora hacen swap

VersiónComportamiento ante 4xx/5xx
htmx 2No se hace swap; hay que interceptar htmx:beforeSwap
htmx 4Se hace swap con todos los códigos excepto 204 y 304

En htmx 2, devolver un 422 con el HTML de los errores de validación no mostraba nada, y todo el mundo terminaba escribiendo el mismo listener global para permitirlo. htmx 4 asume el caso común: si el servidor se tomó el trabajo de devolver HTML, es porque quiere que se vea.

Para restaurar el comportamiento anterior:

htmx.config.noSwap = [204, 304, '4xx', '5xx'];

noSwap acepta códigos exactos y comodines.

hx-status: comportamiento por código

Nuevo en htmx 4. Permite declarar qué hacer con cada código HTTP directamente en el elemento:

<form hx-post="/guardar"
      hx-target="#formulario"
      hx-status:422="swap:innerHTML target:#errores select:#validacion"
      hx-status:5xx="swap:none push:false">

</form>

Formato del selector de código

PatrónCoincide con
404Exactamente 404
50x500–509
4xx400–499
5xx500–599

Se evalúan en orden de especificidad: un hx-status:404 gana sobre un hx-status:4xx.

Claves disponibles

ClaveQué controla
swapEstrategia de swap para ese código
targetDestino para ese código
selectQué parte de la respuesta usar
pushSi se empuja la URL al historial
replaceSi se reemplaza la URL
transitionSi se usa View Transition

Ejemplo completo

<form id="alta-usuario"
      hx-post="/usuarios"
      hx-target="#lista"
      hx-swap="beforeend"

      hx-status:422="target:#alta-usuario swap:outerMorph"
      hx-status:409="target:#avisos swap:afterbegin"
      hx-status:401="target:body swap:innerHTML"
      hx-status:5xx="target:#avisos swap:afterbegin">
  <input name="email">
  <button type="submit">Crear</button>
</form>

Lectura directa: si todo va bien, la respuesta se añade a la lista; si hay error de validación, el formulario se recompone con morphing; si hay conflicto o error de servidor, aparece un aviso arriba; si expiró la sesión, se pinta la página de login completa.

Todo eso sin una línea de JavaScript.

flowchart TD
    R[Respuesta del servidor] --> C{Código}
    C -->|200| A["#lista beforeend"]
    C -->|422| B["#alta-usuario outerMorph"]
    C -->|409| D["#avisos afterbegin"]
    C -->|401| E["body innerHTML"]
    C -->|5xx| F["#avisos afterbegin"]
    C -->|204 / 304| G[sin swap]

Convenciones de códigos en una app htmx

CódigoCuándo usarloQué devolver
200Éxito con contenidoEl fragmento actualizado
201Recurso creadoEl fragmento del recurso nuevo
204Éxito sin cambios visiblesNada
400Petición malformadaMensaje de error
401No autenticadoFormulario de login o HX-Redirect
403Sin permisosMensaje
404No existeMensaje o estado vacío
409Conflicto (duplicado, versión)Mensaje con la opción de resolver
422Validación fallidaEl formulario con los errores
5xxError del servidorMensaje genérico

204 es especialmente útil: “hice lo que pediste y no hay nada que actualizar”. Es lo correcto para un DELETE cuando el elemento se quita con hx-swap="delete".

Headers de respuesta que alteran el comportamiento

El servidor puede sobrescribir casi cualquier decisión del cliente.

HeaderEfecto
HX-RetargetCambia el destino del swap
HX-ReswapCambia la estrategia (acepta modificadores)
HX-ReselectCambia qué parte de la respuesta se usa
HX-TriggerDispara eventos en el cliente
HX-Push-UrlEmpuja una URL al historial
HX-Replace-UrlReemplaza la URL actual
HX-LocationNavegación del lado cliente sin recarga
HX-RedirectRedirección completa del navegador
HX-Refreshtrue fuerza recarga total de la página

Redirigir el error a otro sitio

if (!usuario) {
  return new Response(`<p class="error">Tu sesión expiró</p>`, {
    status: 401,
    headers: {
      "Content-Type": "text/html",
      "HX-Retarget": "#avisos",
      "HX-Reswap": "afterbegin",
    },
  });
}

Esto evita tener que anticipar en el HTML todos los destinos posibles de error.

HX-Redirect vs HX-Location

// Recarga completa del navegador
headers: { "HX-Redirect": "/login" }

// Navegación del lado cliente: htmx pide la URL y hace swap
headers: { "HX-Location": "/tareas" }

// Con opciones en HCON
headers: { "HX-Location": 'path:"/tareas" target:"#main" swap:"innerHTML"' }

Usa HX-Redirect cuando el estado del navegador debe reiniciarse (login, logout). Usa HX-Location cuando quieres navegar sin perder lo que ya está cargado.

Notificar eventos desde el error

return new Response(fragmento, {
  status: 409,
  headers: {
    "Content-Type": "text/html",
    "HX-Trigger": '{"mostrarToast": {"tipo": "error", "texto": "Ya existe ese RUT"}}',
  },
});

En el cliente:

<div id="toasts"
     hx-on:mostrarToast="agregarToast(event.detail.tipo, event.detail.texto)">
</div>

Timeouts

htmx 4 tiene un timeout por defecto de 60 000 ms (en htmx 2 era 0, sin límite).

<!-- Global -->
<meta name="htmx-config" content="defaultTimeout:10000">

<!-- Por subárbol o elemento -->
<button hx-post="/informe-pesado" hx-config="timeout:300000">Generar</button>

Cuando se agota, htmx emite htmx:error (y htmx:abort). Manejo global:

htmx.on("htmx:error", () => {
  mostrarToast("La operación tardó demasiado o no hay conexión.");
});

Errores de red

Un error de red (servidor caído, sin conexión) no produce una respuesta HTTP, así que no lo captura hx-status. Se maneja con el evento:

htmx.on("htmx:error", (e) => {
  document.querySelector("#estado-conexion").textContent = "Sin conexión";
});

htmx.on("htmx:after:request", () => {
  document.querySelector("#estado-conexion").textContent = "";
});
flowchart TD
    P[Petición] --> Q{¿Hubo respuesta HTTP?}
    Q -->|No: red caída, timeout| E["htmx:error"]
    Q -->|Sí| S{¿Código?}
    S -->|2xx| OK["htmx:after:request + swap"]
    S -->|4xx / 5xx| RE["htmx:response:error + swap<br/>+ reglas de hx-status"]

Página de error genérica

Un patrón robusto: una zona de avisos en el layout y una regla global de hx-status heredada.

<body hx-status:5xx:inherited="target:#avisos swap:afterbegin"
      hx-status:401:inherited="target:body swap:innerHTML">

  <div id="avisos" aria-live="polite"></div>
  <main id="contenido">…</main>
</body>

Cualquier petición de la aplicación que falle con 500 pinta el aviso en el mismo sitio, y cualquier 401 reemplaza la página con el login. Sin duplicar nada.

Errores comunes

SíntomaCausaSolución
El 422 no muestra los erroresConfig heredada de htmx 2 en noSwapRevisar htmx.config.noSwap
El 500 pinta HTML feo del frameworkEl servidor devuelve su página de errorDevolver un fragmento propio para peticiones con HX-Request
El timeout corta subidas grandesDefault de 60 shx-config="timeout:…" en ese elemento
HX-Redirect no hace nadaSe envió junto con un 204 que no se procesaUsar 200 con cuerpo vacío
El error se inserta donde no correspondeNo hay regla por códigohx-status:<código> o HX-Retarget

Resumen

  • htmx 4 hace swap con todos los códigos salvo 204 y 304.
  • hx-status:<código> declara target, swap, select y push por código HTTP.
  • Los patrones aceptan 404, 50x, 4xx, 5xx, evaluados por especificidad.
  • HX-Retarget, HX-Reswap y HX-Reselect permiten al servidor decidir el destino.
  • HX-Redirect recarga; HX-Location navega del lado cliente.
  • El timeout por defecto es de 60 s y se ajusta con hx-config="timeout:…".
  • Los errores de red no tienen código: se manejan con el evento htmx:error.

Siguiente: Historial y boost →.