Si Caddy responde 431 Request Header Fields Too Large después de actualizar, la causa es el límite de 16 KiB para las cabeceras que llegó con la 2.11.6; antes era 1 MB. Súbelo con max_header_size dentro de servers en las opciones globales. La misma versión descarta sin avisar las cabeceras cuyo nombre lleva un punto, corta a los 60 s las subidas y respuestas que se quedan paradas y deja de heredar client_auth de un sitio comodín. Lo he reproducido el 18 de octubre de 2026 en Docker con caddy:2.11.4 y caddy:2.11.7 y el mismo Caddyfile, y aquí tienes la opción que arregla cada cambio. La guía también está en inglés.

Puntos clave

  • Caddy 2.11.6 (1 de octubre de 2026) limita las cabeceras de la petición a 16 KiB. En mi prueba por HTTP/1.1, la 2.11.7 aceptó una cookie de 20 397 bytes y respondió 431 con un byte más; por HTTP/2 el corte llegó en 16 377 bytes.
  • Las cabeceras con . en el nombre, como X-Api.Key, ya no llegan al backend y no queda rastro en el registro. Se recuperan una a una con expected_dot_headers.
  • Una subida que se queda 75 s sin enviar datos terminó en 504 Gateway Timeout a los 60 s. read_body_idle en timeouts o la directiva timeouts por ruta lo arreglan.
  • Un subdominio con su propio bloque ya no hereda el TLS mutuo (mTLS) de *.dominio. Si lo dabas por hecho, ese sitio quedó abierto sin certificado de cliente.
  • No existe imagen caddy:2.11.5: la etiqueta v2.11.5 se creó el 30 de septiembre sin binarios ni notas. En Docker el salto es de 2.11.4 a 2.11.6 o 2.11.7.
  • Usa la 2.11.7 (3 de octubre): corrige un fallo de la 2.11.6 que cortaba a los 60 s los flujos SSE abiertos con POST. A 18 de octubre de 2026 no hay versión posterior.

¿Qué significa 431 Request Header Fields Too Large en Caddy?

El código 431 indica que el servidor rechaza la petición porque sus cabeceras, juntas o una sola, superan el tamaño que acepta. Caddy está escrito en Go y hasta la 2.11.4 usaba el límite por defecto de su servidor HTTP, 1 MB. Las notas de la versión 2.11.6 lo explican así: "Request headers are limited to 16 KiB by default. Before, we used Go’s 1 MB default." Lo que se ve en el navegador, o con curl -i, es esto:

HTTP/1.1 431 Request Header Fields Too Large
Content-Type: text/plain; charset=utf-8
Connection: close

431 Request Header Fields Too Large

Chromium muestra esa misma línea en texto plano, sin la página de error de Caddy, porque Go responde antes de que la petición llegue a ningún manejador. Por eso tampoco aparece en el registro de acceso de Caddy. Los culpables habituales son las cookies que acumulan dos o tres aplicaciones bajo el mismo dominio y los tokens grandes en Authorization, como un JWT de un proveedor de identidad que lista cientos de grupos.

Cómo he reproducido los cambios de Caddy 2.11.6

Monté tres contenedores de Caddy con las imágenes oficiales de Docker Hub: 2.11.4, 2.11.6 y 2.11.7. Los tres van delante del mismo backend, un servidor Python que devuelve las cabeceras recibidas y el tamaño del cuerpo. Todo corrió en Docker 29.5.2 sobre Linux arm64, con curl 8.14.1 y PHP-FPM 8.4.26 para la prueba de PHP. El Caddyfile común tiene un proxy por HTTP y dos sitios TLS con certificados internos:

http://:80 {
    reverse_proxy echo:8000
}

*.lab.test {
    tls internal {
        client_auth {
            mode require_and_verify
            trust_pool file /lab/ca.pem
        }
    }
    respond "wildcard site {host}, mTLS ok"
}

admin.lab.test {
    tls internal
    respond "admin panel {host}"
}

Quería probar la 2.11.5, pero docker pull caddy:2.11.5 devuelve not found. La etiqueta v2.11.5 existe en el repositorio desde el 30 de septiembre de 2026, sin notas y solo con el código fuente, y la 2.11.6 salió al día siguiente. Si vienes de Docker, tu última versión anterior al cambio es la 2.11.4.

Cómo arreglar el 431: sube max_header_size

max_header_size es una opción de servers en el bloque global y acepta tamaños como 64KiB. Con este bloque al principio del Caddyfile, la 2.11.7 volvió a aceptar 12 cookies de 3500 bytes (unos 42 KB) por HTTP/1.1 y por HTTP/2:

{
    servers {
        max_header_size 64KiB
    }
}

Antes de elegir el valor, ten en cuenta que el límite real depende del protocolo. Por HTTP/1.1, el servidor de Go suma 4096 bytes de margen al leer las cabeceras ("bufio slop" en su código fuente). Por eso, con el valor por defecto, la 2.11.7 aceptó una cookie de 20 397 bytes y devolvió 431 con 20 398.

Por HTTP/2 no hay margen: el último valor aceptado fue 16 377 bytes, y a partir de 16 697 bytes Go ya no responde 431, sino que cierra la conexión. Con curl eso se ve así:

curl: (16) Error in the HTTP2 framing layer

Si tus usuarios ven un error de protocolo HTTP/2 en el navegador en lugar del 431, la causa y el arreglo son los mismos. Mide el tamaño real que necesitas antes de subirlo sin más: un límite más alto también deja entrar peticiones más grandes de clientes maliciosos.

Cabeceras con punto que desaparecen: expected_dot_headers

Desde la 2.11.6, Caddy descarta las cabeceras de la petición cuyo nombre contiene un punto, igual que la 2.11.4 empezó a descartar las que llevan guion bajo. Envié X-Api.Key: s3cr3t a las dos versiones: la 2.11.4 la entregó al backend y la 2.11.7 no, sin una línea en el registro. El motivo es PHP. Comprobé con PHP-FPM detrás de php_fastcgi que PHP convierte el punto en guion bajo, y en la 2.11.4 una petición con las dos cabeceras sobrescribe la legítima:

curl -s -H "X-Api-Key: real-key" -H "X-Api.Key: injected" \
  http://127.0.0.1:30803/

En la 2.11.4, $_SERVER['HTTP_X_API_KEY'] vale injected; en la 2.11.7, real-key. Si tu aplicación espera de verdad una cabecera con punto, permítela por su nombre:

{
    servers {
        expected_dot_headers X-Api.Key
    }
}

La opción admite una lista de nombres y un * final como prefijo, y no distingue mayúsculas. Ojo con un efecto que comprobé: cuando una cabecera con punto está permitida, Caddy descarta su variante con guiones. Con X-Api.Key permitida y las dos cabeceras en la petición, el backend solo recibió X-Api.key: injected. Permite solo los nombres que tu aplicación usa con punto.

Subidas cortadas a los 60 s: read_body_idle y write_idle

La 2.11.6 añadió dos plazos de inactividad por defecto, de 1 minuto cada uno. read_body_idle corta la lectura del cuerpo si el cliente deja de enviar datos y write_idle corta la respuesta si una escritura no avanza. Una pausa entre escrituras no cuenta, así que un flujo SSE (Server-Sent Events) que envía un evento cada 10 s no se ve afectado.

Para probarlo, mi script envía la mitad de un cuerpo de 20 bytes, espera 75 s y envía el resto. En la 2.11.4 la petición terminó en 200; en la 2.11.7, en 504 Gateway Timeout a los 60 s, con esta línea en el registro:

"msg":"readfrom tcp 172.23.0.5:54346->172.23.0.3:8000:
  read tcp 172.23.0.5:80->172.23.0.1:46344: i/o timeout"
"duration":60.000733277,"status":504

Esto afecta a clientes que suben archivos desde redes lentas y que pausan, a uploads reanudables y a herramientas que abren la petición antes de tener los datos. Hay dos arreglos y probé los dos. El primero amplía el plazo para todo el servidor:

{
    servers {
        timeouts {
            read_body_idle 5m
        }
    }
}

El segundo afina por ruta con la directiva timeouts, que es experimental. Los plazos del servidor se aplican después y mandan, así que la documentación pide desactivar el global con un valor negativo. Con esta configuración, la subida parada 75 s en /upload terminó en 200 y la de /api/import en 504:

{
    servers {
        timeouts {
            read_body_idle -1s
        }
    }
}

http://:80 {
    timeouts /upload* {
        read_timeout 5m
    }
    timeouts {
        read_timeout 1m
    }
    reverse_proxy echo:8000
}

Me quedo con la segunda: mantiene la protección contra slowloris en todo lo demás. Para las respuestas, write_idle funciona igual que read_body_idle.

client_auth ya no se hereda del sitio comodín

Desde la 2.11.6 (PR 7920), el client_auth de un sitio comodín ya no se aplica a un nombre de host más concreto que tenga su propio bloque. Con el Caddyfile de arriba, admin.lab.test pedía certificado de cliente en la 2.11.4 porque *.lab.test lo exigía; curl sin certificado fallaba con tlsv13 alert certificate required. En la 2.11.7 la misma petición sin certificado devolvió admin panel admin.lab.test.

El cambio corrige un fallo real: un sitio que nunca configuró mTLS lo recibía por casualidad. Pero si contabas con esa herencia para proteger un panel, al actualizar se queda abierto. Repite client_auth en cada bloque con un fragmento reutilizable:

(lab_mtls) {
    client_auth {
        mode require_and_verify
        trust_pool file /lab/ca.pem
    }
}

admin.lab.test {
    tls internal {
        import lab_mtls
    }
    respond "admin panel {host}"
}

Con el fragmento importado también en *.lab.test, la 2.11.7 volvió a rechazar admin.lab.test sin certificado y a servirlo con él.

Otros cambios incompatibles de Caddy 2.11.6 que no he probado

Las notas de la 2.11.6 incluyen más cambios que pueden romper una configuración y que no he reproducido. Te los dejo para revisar antes de actualizar:

  • Go 1.26: es la versión mínima para compilar Caddy y sus plugins, lo que afecta si construyes con xcaddy o la imagen builder
  • Validación más estricta: rechaza named_routes duplicadas, un uri repetido en forward_auth, pesos no válidos en weighted_round_robin y entradas ambiguas en map
  • API de administración /load: una configuración no válida devuelve 400 con un JSON válido, donde antes devolvía 200
  • Matcher method: normaliza a mayúsculas, así que method get coincide con peticiones GET

¿Qué cambió en Caddy 2.11.7 y hay una versión posterior?

La 2.11.7 salió el 3 de octubre de 2026 para corregir regresiones de la 2.11.6. Sus notas lo dicen sin rodeos: "If you’re on 2.11.6, we recommend upgrading." No toca el límite de cabeceras, las cabeceras con punto ni client_auth. Lo que corrige son tres fallos de los plazos nuevos:

  • un error de puntero nulo que tumbaba Caddy al hacer de proxy por HTTP/2
  • los flujos HTTP/2 que se reiniciaban tras 1 minuto en silencio
  • las respuestas en streaming a peticiones con cuerpo, que se cortaban a los 60 s

Esto último lo reproduje con un flujo SSE abierto con POST que emite un evento cada 10 s durante 100 s. La 2.11.6 lo cortó a los 60 s y curl terminó con el código 18 (transferencia incompleta); la 2.11.4 y la 2.11.7 lo completaron. A 18 de octubre de 2026, la 2.11.7 es la última versión: no hay etiqueta 2.11.8 ni 2.12 en el repositorio, y caddy:2, caddy:2.11 y caddy:latest apuntan a ella.

Cómo actualizar Caddy en Docker sin sorpresas

El orden importa, porque expected_dot_headers y read_body_idle no existen en la 2.11.4. Si añades esas opciones antes de cambiar la imagen, la 2.11.4 no arranca:

Error: adapting config using caddyfile: parsing caddyfile tokens
for 'servers': unrecognized servers option 'expected_dot_headers'

max_header_size sí existía y la 2.11.4 lo acepta, así que puedes ponerlo antes. Para lo demás, valida el Caddyfile nuevo con la imagen nueva antes de tocar el contenedor en marcha:

docker run --rm \
  -v "$PWD/Caddyfile":/etc/caddy/Caddyfile:ro \
  caddy:2.11.7 caddy validate \
  --config /etc/caddy/Caddyfile --adapter caddyfile

Si imprime Valid configuration, cambia la etiqueta en tu compose.yml a caddy:2.11.7 y recrea el servicio con docker compose up -d. Si te quedas en la misma imagen y solo cambias el Caddyfile, docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile lo aplica sin reiniciar. Si Caddy está delante de otros servicios, como en la guía de Forgejo con Docker y Caddy, prueba después un inicio de sesión real: es donde aparecen las cookies grandes.

Quien instala Caddy con apt tuvo además dos tropiezos ajenos al código. El 30 de septiembre, apt rechazó el repositorio de Cloudsmith con EXPKEYSIG 531A6B20FA058A70 porque firmaba con una subclave caducada en 2024; Cloudsmith lo corrigió el 1 de octubre (issue 8095).

Entre el 9 y el 11 de octubre, el mismo repositorio devolvió 402 Payment Required por agotar la cuota de tráfico (dist 142). Un mantenedor habló de IP con unas 560 000 peticiones al mes desde sistemas de integración continua. Ninguno de los dos afecta a la imagen de Docker.

Preguntas frecuentes

¿Puedo volver al límite de 1 MB de antes?

Sí: max_header_size 1MB en servers restaura el comportamiento de la 2.11.4. No lo recomiendo salvo que lo necesites, porque el límite bajo protege de peticiones con cabeceras enormes. En mi prueba, 64 KiB cubrió 12 cookies de 3500 bytes, más de lo que suman las cookies de las aplicaciones que tengo bajo un dominio.

¿Me afecta si uso Traefik o Nginx Proxy Manager delante de Caddy?

El límite se aplica en el Caddy que recibe la petición. Si otro proxy, como Traefik o Nginx Proxy Manager, reenvía a Caddy, las cabeceras llegan igual y el 431 sale de Caddy. Ese proxy tiene además su propio límite, así que revisa los dos.

¿Por qué no veo el 431 en el registro de acceso de Caddy?

Porque el servidor HTTP de Go responde antes de pasar la petición a Caddy, y por eso no hay entrada de acceso ni de error. Para confirmar que la causa es el tamaño, repite la petición con curl -i y una cabecera grande, o compara la respuesta con la 2.11.4.

Conclusión

Caddy 2.11.6 cambió cuatro valores por defecto en una versión de parche, y los cuatro se arreglan con una línea del Caddyfile. max_header_size devuelve el margen para cookies y tokens grandes, expected_dot_headers recupera las cabeceras con punto que necesites y read_body_idle o la directiva timeouts amplían el plazo de subida donde haga falta. client_auth hay que repetirlo en cada bloque que deba pedir certificado. Actualiza directamente a la 2.11.7, valida el Caddyfile con la imagen nueva antes de cambiarla y prueba un inicio de sesión real después del cambio.

Fuentes: [1] Caddy, notas de la versión v2.11.6[1], [2] Caddy, notas de la versión v2.11.7[2], [3] Documentación de Caddy, opciones globales y bloque servers[3], [4] Documentación de Caddy, directiva timeouts[4], [5] Caddy, PR 7920 sobre client_auth y comodines[5], [6] Go, net/http server.go y el margen de lectura de cabeceras[6], [7] Caddy, issue 8095 sobre EXPKEYSIG en el repositorio apt[7], [8] caddyserver/dist, issue 142 sobre 402 Payment Required[8], [9] MDN, código de estado 431[9].

Fuentes

  1. Caddy, notas de la versión v2.11.6
  2. Caddy, notas de la versión v2.11.7
  3. Documentación de Caddy, opciones globales y bloque servers
  4. Documentación de Caddy, directiva timeouts
  5. Caddy, PR 7920 sobre client_auth y comodines
  6. Go, net/http server.go y el margen de lectura de cabeceras
  7. Caddy, issue 8095 sobre EXPKEYSIG en el repositorio apt
  8. caddyserver/dist, issue 142 sobre 402 Payment Required
  9. MDN, código de estado 431