Saltar al contenido
Development

Repensando la tarjeta de GitHub, y un 403 que el build exitoso no detectó

Por Victor Da Luz
astroprivacydev-logsite

La portada tiene una pequeña sección “En GitHub”: una cuadrícula de paquetes compartidos, una tarjeta de estadísticas, y cuatro tarjetas de repos fijados. Las tarjetas de estadísticas y de repos fijados eran imágenes, con hotlink desde un servicio de estadísticas de terceros en Vercel, en cada visita a la portada. Sin puerta de consentimiento, sin opción de exclusión. Es el tipo de cosa que es fácil de agregar y fácil de olvidar que está ahí.

Una revisión de UI/UX lo detectó. La propia página de privacidad del sitio dice que nada rastrea hasta que se da el consentimiento, y ahí había cinco solicitudes disparándose en cada carga de página, todo el tiempo, entregando la IP y el user agent de un desconocido a un servicio que no controlo. Cuando ese servicio está caído, la sección simplemente queda en blanco. Sin error, sin alternativa, solo un espacio vacío y silencioso donde antes había una tarjeta.

La parte que convirtió esto en algo más que un borrado

Antes de tocar nada, revisé qué eran realmente esos cuatro repos fijados: homelab-tools, astro-blog, astro-affiliate, astro-opt-in-analytics. Tres de esos cuatro ya tenían sus propias tarjetas, escritas a mano, localizadas al español, ubicadas en la cuadrícula “Paquetes compartidos” justo encima de las imágenes que estaba por reemplazar. Un simple cambio de imagen por tarjeta habría dejado a astro-blog apareciendo dos veces en la misma página, una vez con texto curado real y otra con la descripción en inglés que la API de GitHub devolviera.

Así que la solución no fue “cambiar cinco imágenes por cinco tarjetas nativas.” Fue: mantener la cuadrícula de paquetes compartidos como el único hogar real de esos tres repos, y averiguar para qué sirve realmente el resto de la sección. Terminé eliminando las tarjetas por repo por completo y reemplazando todo el bloque de estadísticas/fijados por una sola línea: cantidad de repos públicos y cantidad de seguidores, obtenidas de la API REST de GitHub, ubicada justo al lado del enlace al perfil que ya existía.

Obtener esto en tiempo de build, no en tiempo de ejecución

Tanto la portada en inglés como la de español están completamente prerenderizadas (export const prerender = true), así que una llamada fetch() en el frontmatter del componente corre una sola vez, en tiempo de build, y el resultado queda horneado directamente en el HTML estático. Sin solicitud del lado del cliente, sin costo por visitante, sin ningún tercero involucrado. Eso encaja mejor acá que el patrón que había usado para el envío a IndexNow (una integración astro:build:done), porque ese hook corre después de que la página ya se renderizó. Puede sacar datos hacia afuera, pero no puede meter datos dentro de la página.

La trampa: un build limpio que no publicó nada

En el primer intento, el build tuvo éxito, sin errores, sin advertencias. Revisé el HTML compilado en busca del viejo dominio de terceros: había desaparecido, como esperaba. Lo di por terminado en mi cabeza durante unos treinta segundos, y después me fijé de verdad en la línea de estadísticas en sí. No estaba. Ni en /, ni en /es/.

curl https://api.github.com/users/vdaluz desde mi propia terminal devolvió un 200 limpio con JSON real. Así que la API estaba bien, la red estaba bien, y sin embargo la misma solicitud exacta desde dentro del build de Astro estaba fallando en silencio y quedando absorbida por un try/catch que simplemente devolvía el resultado a null.

Resultó que curl envía su propio User-Agent por defecto, y fetch() no envía ninguno a menos que se configure explícitamente. La API REST de GitHub devuelve un 403 directo a cualquier solicitud sin encabezado User-Agent. Mi try/catch estaba funcionando exactamente como fue diseñado, capturando un fallo y degradándose con elegancia, que es exactamente también cómo ocultó el bug. response.ok simplemente era falso, sin excepción, sin ruido en la consola, nada con qué tropezar.

El arreglo fue de una sola línea: configurar los encabezados User-Agent y Accept explícitamente en la llamada fetch. Pero la lección real estaba en cómo había estado revisando mi propio trabajo. “Lo viejo y malo desapareció” y “lo nuevo y bueno está presente” son dos afirmaciones distintas, y un build que en silencio no cumple ninguna de las dos pasa la primera comprobación con la misma facilidad que uno correcto. Volví atrás y busqué específicamente con grep los números reales (“28 public repos”) en el HTML compilado antes de confiar en él, y eso fue lo que detectó el problema.

Lo que se publicó

Se eliminaron las imágenes con hotlink y los datos ahora sin uso de pinnedRepos. Una línea de estadísticas nativa, del mismo origen, construida a partir de una sola llamada REST sin autenticación, que se degrada a nada (no a ceros, no a datos viejos) si la llamada a la API falla por cualquier motivo. El viejo dominio de estadísticas se eliminó de la lista permitida img-src de la CSP tanto en el archivo estático de encabezados como en el middleware de SSR, ya que nada lo referencia más. Y verificación contra el sitio en vivo después del deploy, no solo en localhost: hice curl a producción, confirmé que los números reales estaban horneados y que el dominio viejo había desaparecido por completo de la respuesta.

Función pequeña, pero es un buen recordatorio de que “¿el build pasó?” y “¿la función realmente funcionó?” no son la misma pregunta, sobre todo en cuanto entra en juego un try/catch.

Lecturas relacionadas

Development

El CTA que apuntaba al dev log equivocado

El único llamado a la acción de la página de Deep Cut Atlas enlazaba al blog completo sin filtrar, un enlace que funcionaba, devolvía 200, y en silencio mandó a todos al lugar equivocado durante semanas.

Leer