Skip to content

Pipeline de Generación de PDFs

Tipo: Decisión Arquitectónica Estado: En Uso (desde junio 2026) Alcance: Servicio informes + microservicio pdf-renderFecha: 2026-06-26


Nota de alcance — dos motores de renderizado

informes expone dos motores de generación de PDF:

  • Puppeteer / pdf-render (43 endpoints, vía PdfGeneratorService): el HTML se envía al microservicio pdf-render via POST HTTP. La sección de estrategia de espera 'load' que se describe más abajo aplica únicamente a esta ruta.
  • Dompdf en proceso (23 endpoints, vía new PDFGenerator(), isRemoteEnabled=true, p. ej. facturas fiscales A4): el PDF se genera directamente dentro del contenedor informes, sin pasar por pdf-render. La estrategia 'load' no aplica a esta ruta.

La regla del HTML autocontenido (CSS inline via reportStyles(), imágenes estáticas como data URIs Base64 via assetDataUri()) beneficia ambos motores. Sin embargo, las imágenes dinámicas por tenant en la ruta Dompdf ($logoFactura, pie de página, QR) quedan intencionalmente como recursos remotos en esa ruta y no deben convertirse a Base64.


Descripción general

El sistema de informes genera PDFs a través de un pipeline de dos servicios:

  1. informes (PHP/Apache) ensambla el HTML del reporte y lo envía mediante un POST HTTP.
  2. pdf-render (Node.js / Puppeteer, puerto 3000) recibe el HTML, lo carga en Chromium via page.setContent() y devuelve el PDF binario.
mermaid
sequenceDiagram
    participant C as Cliente (bautista-backend)
    participant I as informes (PHP)
    participant R as pdf-render :3000 (Node/Puppeteer)
    participant Ch as Chromium

    C->>I: GET /reports/…
    I->>I: Ensambla HTML<br/>(CSS inline + imágenes base64)
    I->>R: POST /to-pdf/ { html: "…" }
    R->>Ch: page.setContent(html)
    Ch-->>R: waitUntil: 'load' ✓
    R-->>I: PDF binario
    I-->>C: Content-Type: application/pdf

Regla: el HTML debe ser completamente autocontenido

Todo HTML que salga de informes hacia pdf-render debe embeber:

  • Todo el CSS mediante el partial reportStyles() (informes/components/report-styles.php), que incrusta Bootstrap 5.2.2 vendorizado como un bloque <style> inline.
  • Todas las imágenes como data URIs Base64 mediante el helper assetDataUri(string $relativePath): string (informes/util/assets.php).

No debe haber ninguna URL externa (CDN, frontend, ni ningún host remoto) en el <body> del HTML enviado a pdf-render.

Razón técnica

page.setContent() de Puppeteer carga el HTML directamente en el contexto de red del contenedor pdf-render, no en el contexto del contenedor informes. Desde pdf-render, la URL https://cdn.jsdelivr.net/… es alcanzable en teoría, pero:

  • La red del contenedor puede no tener salida a internet en producción.
  • Cualquier recurso externo que no resuelva inmediatamente mantiene conexiones abiertas, lo que impacta directamente en la estrategia de espera (ver sección siguiente).

La única forma de garantizar que el HTML se renderice correctamente, con independencia del entorno de red, es que todo el CSS e imágenes estén incrustados antes de que el HTML salga de informes.

Helpers de autocontenido

HelperArchivoQué hace
reportStyles()informes/components/report-styles.phpLee assets/css/bootstrap-5.2.2.min.css del disco (caché static), lo envuelve en <style>…</style> y retorna el string. Sin HTTP, sin lectura repetida por llamada.
assetDataUri(string $relativePath)informes/util/assets.phpResuelve $relativePath bajo assets/, detecta MIME con finfo, codifica en Base64 y retorna data:<mime>;base64,…. Caché static array por path. Rechaza traversal ...

Estrategia de espera: 'load'

pdf-render usa waitUntil: 'load' al llamar a page.setContent().

El evento load dispara cuando el documento HTML y todos los recursos referenciados en él (CSS, imágenes) han terminado de cargarse. Cuando el HTML es autocontenido (CSS inline, imágenes Base64), no hay recursos externos que esperar: el evento load resuelve de inmediato, y el tiempo de generación del PDF está acotado únicamente por el tiempo de renderizado de Chromium.

Por qué 'domcontentloaded' es inseguro

DOMContentLoaded dispara cuando el árbol DOM está construido, pero antes de que las hojas de estilo externas terminen de cargarse. En Chromium, una etiqueta <link rel="stylesheet" href="…"> no bloquea DOMContentLoaded a menos que haya un <script> bloqueante posterior en el documento.

Consecuencia: si algún render file reintroduce un <link> CSS, Puppeteer puede capturar la página antes de que los estilos se hayan aplicado, generando un PDF sin formato. Este modo no es seguro para reportes fiscales.

Por qué 'networkidle0' fue la causa raíz del problema

networkidle0 considera la página lista cuando no hay conexiones de red activas durante 500 ms consecutivos. Antes de la corrección, los render files referenciaban Bootstrap desde el CDN de jsDelivr:

html
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">

Desde el contenedor pdf-render, el host cdn.jsdelivr.net era inalcanzable (o de resolución lenta). Chromium mantenía esa conexión abierta indefinidamente, impidiendo que se cumpliera la condición de "500 ms sin conexiones". El resultado era un stall de varios minutos hasta que Puppeteer alcanzaba el timeout de la operación.

Resumen comparativo:

EstrategiaCuándo resuelveSeguro con HTML autocontenidoSeguro con recursos externos
domcontentloadedÁrbol DOM construido (antes de CSS externo)No — riesgo de PDF sin estilosNo
loadDOM + todos los recursos cargadosSí — resuelve inmediatamenteDepende del host
networkidle0500 ms sin conexiones activasNo — stall si un host no responde

Gotcha crítico para mantenedores

Cualquier <link> o <img src="https://…"> externo reintroducido en un render file re-arma el stall bajo 'load' y causará un timeout de generación.

'load' espera que todos los recursos del HTML terminen de cargar. Si se agrega un <link> CDN o una imagen con URL externa:

  • Bajo condiciones de red normales, el PDF tardará el tiempo de resolución del recurso externo.
  • Bajo condiciones adversas (CDN inalcanzable, red del contenedor sin salida a internet), el PDF fallará por timeout, reproduciendo exactamente el bug original.

La invariante que debe preservarse en todos los render files (informes/reports/**/*-render.php):

✅ <style> con CSS inline (via reportStyles())
✅ <img src="data:image/…;base64,…"> (via assetDataUri())
❌ <link href="https://…"> — PROHIBIDO
❌ <img src="https://…"> — PROHIBIDO
❌ <img src="http://…"> — PROHIBIDO

Orden de deploy obligatorio

El deploy de esta corrección debe respetar el siguiente orden:

1. PR informes (WU 1-3): CSS inline + imágenes Base64
       ↓  (merge y deploy en producción)
2. PR pdf-render (WU 4): waitUntil: 'load'

No invertir el orden. Si pdf-render se despliega con 'load' mientras informes aún envía HTML con el <link> CDN, el PDF fallará por timeout: 'load' esperará que el CDN responda, y la red del contenedor no puede garantizarlo.

El rollback, si fuera necesario, sigue el orden inverso: revertir pdf-render primero, luego informes.


Referencias


Última actualización: 2026-06-26