El deploy que subió un CSS de 0 bytes · Daniel Tinizaray

El deploy que subió un CSS de 0 bytes · Daniel Tinizaray

Un martes mi pipeline de blog terminó en verde: build limpio, deploy "exitoso". Al abrir el sitio, el HTML cargaba perfecto pero la página estaba desnuda — cero estilos. El culpable: dos archivos CSS subidos con content-length 0. Este es el post-mortem de ese deploy roto en Cloudflare Pages, con el diagnóstico exacto y la regla que adopté después.

Síntoma: HTML servido, CSS en 404

Mi cron de publicación crea un trigger, ejecuta astro build y confirma el deploy. Ese día el flujo reportó todo verde. Fui a verificar la publicación y me encontré con un sitio sin ningún estilo: tipografía por defecto, sin layout, sin grilla. Abrí DevTools y vi el patrón clásico de assets rotos:

GET /_astro/BaseLayout.CmXjET7T.css → 404
GET /_astro/Projects.tN0RGdOL.css   → 404
GET /index.html                     → 200 OK

El HTML se servía bien, así que el build en sí había funcionado. Los hashes de los archivos CSS eran los correctos (coincidían con los referenciados en el HTML), lo que significaba que el dist/ local estaba sano. El problema estaba entre el build y el edge.

Diagnóstico: content-length 0 en la API de Cloudflare

Antes de asumir cache o propagación, inspeccioné qué había realmente subido. Consulté el manifiesto del deployment directamente en la API de Cloudflare y ahí estaba la evidencia: los dos archivos CSS existían en el deployment, pero con content-length: 0. Bytes cero. El deploy había "subido" los nombres de los archivos sin su contenido.

Con eso el panorama quedó claro. El deploy de esa noche era ad-hoc vía API, no el deploy estándar con wrangler. El trigger y el build sí se ejecutaron, pero el push ad-hoc que confirmé a mano no incluía los assets como tal: subió una referencia vacía por cada asset. Cloudflare no estaba mintiendo con el 404 — el contenido simplemente nunca llegó.

Lo verifiqué comparando contra el build local:

# El dist local tenía los CSS con contenido real
ls -la dist/_astro/*.css
# -rw-r--r-- 14K dist/_astro/BaseLayout.CmXjET7T.css

# El deployment en Cloudflare los tenía en 0 bytes
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/danieltini-dev/deployments" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  | python3 -c "...filtrar assets y content-length..."

Mismo nombre, mismo hash, tamaño distinto. Un deploy parcial disfrazado de exitoso.

Fix: deploy real con wrangler pages deploy

La corrección fue rehacer el deploy por la vía que sí sube el directorio completo de assets:

cd danieltini.dev && \
CLOUDFLARE_API_TOKEN=$TOKEN npx wrangler pages deploy dist/ \
  --project-name danieltini-dev --branch main

wrangler pages deploy sube el dist/ íntegro: valida cada archivo, calcula los hashes y hace el push atómico del bundle. En dos minutos el sitio volvió a estar entero, con los mismos hashes de CSS que el build original — la evidencia de que el problema nunca fue el build, sino el mecanismo de publicación.

¿Por qué no bastaba con purgar el cache? Porque el cache no era el problema: el origen (el deployment en Cloudflare) tenía los archivos vacíos. Purgar habría servido el mismo 404, más rápido. Lección barata de aprender con un curl en lugar de con dos horas de debugging de cache.

Regla final: el deploy es un comando, no una confirmación

Saqué una regla operativa que ya apliqué a todos los crons de publicación: el deploy real lo ejecuta el CLI, no una cadena de trigger y confirmación. Un trigger que dispara un build y una confirmación que "debería" publicar no son equivalentes a wrangler pages deploy. Si el paso de publicación no es un comando idempotente que sube el bundle completo, el pipeline miente.

Las dos verificaciones que agregué al flujo:

  • Post-deploy check: un curl -s -o /dev/null -w '%{http_code}' contra un asset con hash (/_astro/*.css) además del HTML. Un 200 en HTML no garantiza nada sobre los assets.
  • Content-length, no solo status: en el asset crítico, verificar que el tamaño sea > 0. Un 200 con 0 bytes es peor que un 404 visible, porque el 404 al menos grita.

Lo que aprendí

  • "Deploy exitoso" no significa "assets servidos": los dos CSS llegaron con content-length 0 y el pipeline reportó verde. El éxito de un deploy se mide sobre los assets, no sobre el log.
  • La API de Cloudflare fue la mejor fuente de verdad: el manifiesto del deployment mostró los 0 bytes en segundos, antes de tocar cache, DNS o configuración.
  • wrangler pages deploy es el único camino para publicar este sitio. Los deploys ad-hoc vía API quedaron fuera del flujo permanente; cada excepción ad-hoc es una apuesta que ya perdí una vez.
  • Un post-mortem de 30 minutos después del incidente vale más que cualquier runbook teórico: esta nota es el runbook.

Más de cómo opero esta infraestructura en danieltini.dev y en mi GitHub.


¿Te gustó este artículo?

Si estás lidiando con estos desafíos en tu empresa, hablemos. Sin compromiso. 30 minutos para entender tu situación.

Agenda una llamada gratuita →