Appearance
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
informesexpone dos motores de generación de PDF:
- Puppeteer / pdf-render (43 endpoints, vía
PdfGeneratorService): el HTML se envía al microserviciopdf-rendervia 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 contenedorinformes, sin pasar porpdf-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 viaassetDataUri()) 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:
informes(PHP/Apache) ensambla el HTML del reporte y lo envía mediante un POST HTTP.pdf-render(Node.js / Puppeteer, puerto 3000) recibe el HTML, lo carga en Chromium viapage.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/pdfRegla: 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
| Helper | Archivo | Qué hace |
|---|---|---|
reportStyles() | informes/components/report-styles.php | Lee 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.php | Resuelve $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:
| Estrategia | Cuándo resuelve | Seguro con HTML autocontenido | Seguro con recursos externos |
|---|---|---|---|
domcontentloaded | Árbol DOM construido (antes de CSS externo) | No — riesgo de PDF sin estilos | No |
load | DOM + todos los recursos cargados | Sí — resuelve inmediatamente | Depende del host |
networkidle0 | 500 ms sin conexiones activas | Sí | No — 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://…"> — PROHIBIDOOrden 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
- Código fuente:
informes/components/report-styles.php,informes/util/assets.php - Implementación pdf-render:
pdf-render/src/service/PdfService.ts(línea ~185) - Arquitectura General del Sistema
- Consolidación de Informes Multi-Schema
Última actualización: 2026-06-26