Implementar recuperación de errores y estrategias de reintento personalizadas
De un vistazo
Sección titulada «De un vistazo»Un servicio de documentos en producción hace más que capturar una excepción y registrarla. Decide qué hacer a continuación: continuar con un resultado degradado, cambiar a una segunda ruta de renderizado, reintentar con una entrada que el motor acepte o entregar las páginas que ya se hubieran construido antes del fallo. Esta recipe presenta cuatro estrategias de recuperación construidas sobre la jerarquía de excepciones de NextPDF y los métodos de inspección del estado del documento:
- Degradación elegante ante un fallo de fuente: capturar
NextPDF\Exception\FontNotFoundException, recurrir a una tipografía garantizada y mantener el documento en construcción. - Un renderer de respaldo: cuando la ruta en proceso
Document::writeHtml()rechaza la entrada, reintentar a través deDocument::writeHtmlChrome(), el puente con Chrome denextpdf/artisan. - Reintento con HTML alternativo: cuando se dispara
NextPDF\Exception\HtmlParsingExceptionoNextPDF\Exception\CssResolutionBudgetExceededException, reintentar con una variante de HTML simplificada y de calidad conocida. - Recuperación de documento parcial: leer
Document::getNumPages()tras un fallo y guardar lo que ya se hubiera construido en lugar de descartarlo.
La captura con la granularidad adecuada ya está cubierta. La página complementaria Maneja errores con la jerarquía de excepciones de NextPDF explica la jerarquía en sí. Esta página cubre qué hacer después de la captura.
Esta recipe está dirigida a la edición core de OSS. Toda la API mencionada aquí vive en nextpdf/core. La única dependencia opcional es nextpdf/artisan para el respaldo con Chrome.
Instalación
Sección titulada «Instalación»composer require nextpdf/core:^3La estrategia del renderer de respaldo usa además el puente con Chrome:
composer require nextpdf/artisanCuando nextpdf/artisan no está presente, Document::writeHtmlChrome() lanza NextPDF\Exception\PageLayoutException en lugar de renderizar, así que la estrategia de respaldo siguiente trata la ausencia del puente como otro caso recuperable.
Visión general conceptual
Sección titulada «Visión general conceptual»La recuperación se apoya en dos hechos sobre NextPDF, ambos verificados en el código fuente.
La jerarquía de excepciones indica qué es recuperable. Toda excepción de dominio extiende la base abstracta NextPDF\Exception\NextPdfException, que extiende RuntimeException e implementa NextPDF\Contracts\ContextAwareExceptionInterface. Capturar un subtipo específico permite elegir una ruta de recuperación asociada al fallo:
FontNotFoundExceptionincluyegetFontName(),getSearchPaths()ywasFallbackAttempted(): suficiente para reintentar con una tipografía distinta.HtmlParsingExceptionincluyegetRule(),getPosition()ygetHtmlSnippet(): suficiente para decidir si vale la pena intentar una variante simplificada.CssResolutionBudgetExceededExceptionincluyegetVisits()ygetBudget(): señal de un selector patológico que una hoja de estilos reducida puede despejar.- Un límite importante:
NextPDF\Support\DegradedExceptionextiendeRuntimeExceptiondirectamente, noNextPdfException. Por lo tanto,catch (NextPdfException $e)no captura un rechazo de la política de degradación. Cuando laNextPDF\Contracts\DegradationPolicyactiva seaStrictoBalanced, hay que capturarDegradedExceptionde forma explícita para recuperarse.
El documento es inspeccionable mientras se construye. Un Document expone su estado de construcción mediante métodos de acceso de solo lectura. getNumPages() devuelve el número total de páginas, incluida la página activa aún no vaciada, y getPage() devuelve el índice de base cero de la página actual. Tras un fallo a mitad de construcción, leer getNumPages() permite saber si existen páginas completas y luego llamar a save() o getPdfData() para emitirlas. El motor también registra eventos de degradación no fatales: getWarnings() devuelve una list<NextPDF\Support\Warning>, hasWarnings() informa si se recopiló alguna y hasDegradedParity() informa si la fidelidad de salida se vio afectada. Esto permite que una rutina de recuperación distinga «se completó de forma limpia» de «se completó con fidelidad reducida» sin analizar ninguna excepción.
La política de degradación determina qué eventos se manejan como excepciones y cuáles como advertencias. NextPDF\Core\Config tiene como valor predeterminado DegradationPolicy::Balanced, que advierte y continúa ante una degradación acotada, pero lanza ante un impacto bloqueante. DegradationPolicy::Permissive nunca lanza y recopila todo en el canal de advertencias. DegradationPolicy::Strict lanza ante cualquier riesgo de conformidad, pérdida semántica o impacto bloqueante. Primero se elige la política y luego se escribe la recuperación para las formas de fallo que esa política produce.
Superficie de la API
Sección titulada «Superficie de la API»El código de recuperación siguiente usa estos miembros verificados:
NextPDF\Core\Document::createStandalone(?Config $config = null): self,addPage(),setFont(string $family, string $style = '', float $size = 12.0): static,cell(...),writeHtml(string $html): static,writeHtmlChrome(string $html, ?float $width = null, ?float $height = null): static,save(string $path): void,getPdfData(): string,getNumPages(): int,getPage(): int,getWarnings(): list<Warning>,hasWarnings(): bool,hasDegradedParity(): bool,addFontDirectory(string $directory): static.NextPDF\Core\Config::withDegradationPolicy(DegradationPolicy $policy): selfy el valor predeterminado dedegradationPolicy,DegradationPolicy::Balanced.NextPDF\Contracts\DegradationPolicy:Strict,Balanced,Permissive.NextPDF\Exception\NextPdfException(base abstracta),NextPDF\Exception\FontNotFoundException,NextPDF\Exception\HtmlParsingException,NextPDF\Exception\CssResolutionBudgetExceededException,NextPDF\Exception\WriterException,NextPDF\Exception\PageLayoutException.NextPDF\Support\DegradedException(que llevacapabilityypolicy),NextPDF\Support\Capability(id,status,reason,isDegraded()),NextPDF\Support\Warning,NextPDF\Support\WarningSeverity.
Ejemplo de código: inicio rápido
Sección titulada «Ejemplo de código: inicio rápido»La recuperación útil mínima: capturar un fallo de fuente ausente, recurrir a una tipografía garantizada y continuar. Este fragmento omite el manejo más amplio del ejemplo de producción. Para ver un manejador completo con registro y el límite de DegradedException, consultar el ejemplo de producción siguiente.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\FontNotFoundException;
$doc = Document::createStandalone();$doc->addPage();
try { // A face that may not be installed on every host. $doc->setFont('CorporateSans', '', 12);} catch (FontNotFoundException $e) { // Recover: fall back to a face the engine always resolves. $doc->setFont('helvetica', '', 12);}
$doc->cell(0, 10, 'Rendered with a recovered font.', newLine: true);$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');Ejemplo de código: producción
Sección titulada «Ejemplo de código: producción»El ejemplo completo conecta las cuatro estrategias en una sola canalización de renderizado: un respaldo de fuente, un renderer de respaldo de la ruta en proceso a Chrome, un reintento con HTML alternativo y una recuperación de documento parcial basada en getNumPages(). Respeta el canal de salida del arnés y nunca captura una Exception sin especializar ni deja un bloque catch vacío.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Contracts\ContextAwareExceptionInterface;use NextPDF\Contracts\DegradationPolicy;use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Exception\CssResolutionBudgetExceededException;use NextPDF\Exception\FontNotFoundException;use NextPDF\Exception\HtmlParsingException;use NextPDF\Exception\NextPdfException;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;use NextPDF\Support\DegradedException;
/** * A minimal structured sink. In production this is your PSR-3 logger; the * exception class and its structured context become log fields. * * @param array<string, mixed> $context */function logRecovery(string $message, array $context): void{ fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");}
/** * Resolve a usable font, degrading from the requested face to a guaranteed * fallback. Returns the face actually applied so the caller can record it. * * @param non-empty-string $requested * @param non-empty-string $fallback * * @return non-empty-string */function applyFontWithFallback(Document $doc, string $requested, string $fallback): string{ try { $doc->setFont($requested, '', 12);
return $requested; } catch (FontNotFoundException $e) { // STRATEGY 1 — graceful degradation on a font failure. logRecovery('Font unavailable; degrading to a guaranteed face', [ 'exception' => $e::class, 'font_name' => $e->getFontName(), 'searched' => $e->getSearchPaths(), 'fallback' => $fallback, ]); $doc->setFont($fallback, '', 12);
return $fallback; }}
/** * Render HTML through the in-process pipeline, then through the Chrome bridge, * then through a simplified HTML variant. Each layer recovers a more specific * failure than the last. */function renderHtmlWithRecovery(Document $doc, string $primaryHtml, string $simplifiedHtml): void{ try { // Primary path: the in-process HTML/CSS pipeline. $doc->writeHtml($primaryHtml);
return; } catch (CssResolutionBudgetExceededException $e) { // STRATEGY 3 — retry with alternative HTML for a pathological selector. logRecovery('CSS resolution budget exceeded; retrying with simplified HTML', [ 'exception' => $e::class, 'visits' => $e->getVisits(), 'budget' => $e->getBudget(), ]); $doc->writeHtml($simplifiedHtml);
return; } catch (HtmlParsingException $e) { // STRATEGY 2 — fall back to the Chrome renderer for input the // in-process parser rejects. The Chrome bridge uses a browser CSS // engine, so it may accept what the in-process parser would not. logRecovery('In-process HTML parse failed; trying the Chrome fallback renderer', [ 'exception' => $e::class, 'rule' => $e->getRule(), 'position' => $e->getPosition(), ]);
try { $doc->writeHtmlChrome($primaryHtml);
return; } catch (PageLayoutException $chromeError) { // The Chrome bridge is absent (nextpdf/artisan not installed) or // rejected the input. Last resort: the simplified HTML variant // through the in-process pipeline. logRecovery('Chrome fallback unavailable; retrying with simplified HTML', [ 'exception' => $chromeError::class, ]); $doc->writeHtml($simplifiedHtml);
return; } }}
// --- Configure the degradation policy up front ---------------------------// Balanced (the default) warns on bounded degradation and throws only on a// blocking impact. A regulated workflow would choose DegradationPolicy::Strict.$config = (new Config())->withDegradationPolicy(DegradationPolicy::Balanced);$doc = Document::createStandalone($config);
$primaryHtml = '<h1>Quarterly report</h1><p>Body paragraph with rich styling.</p>';$simplifiedHtml = '<h1>Quarterly report</h1><p>Body paragraph (simplified).</p>';
$outputPath = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf';
try { $doc->addPage(); $applied = applyFontWithFallback($doc, 'CorporateSans', 'helvetica'); $doc->cell(0, 12, 'Custom error recovery patterns', newLine: true);
renderHtmlWithRecovery($doc, $primaryHtml, $simplifiedHtml);
$doc->save($outputPath);
logRecovery('Document built', [ 'font_applied' => $applied, 'pages' => $doc->getNumPages(), 'has_warnings' => $doc->hasWarnings(), 'degraded_parity' => $doc->hasDegradedParity(), ]);} catch (DegradedException $e) { // BOUNDARY: DegradedException extends RuntimeException directly, NOT // NextPdfException, so the catch-all below would not have caught it. // Under Strict/Balanced policy a blocking degradation lands here. logRecovery('Capability degraded under the active policy; emitting a built partial', [ 'exception' => $e::class, 'capability' => $e->capability->id, 'status' => $e->capability->status->value, 'reason' => $e->capability->reason ?? 'unknown', 'policy' => $e->policy->value, ]); // STRATEGY 4 — partial-document recovery: save whatever pages exist. if ($doc->getNumPages() > 0) { $doc->save($outputPath); }} catch (WriterException $e) { // Serialization or I/O failure: the in-memory document is valid but could // not be written. Surface the stage so infrastructure can act on it. logRecovery('PDF write failed; document was valid in memory', [ 'exception' => $e::class, 'writer_state' => $e->getWriterState(), 'output_path' => $e->getOutputPath(), ]);} catch (NextPdfException $e) { // Catch-all for every other NextPDF\Exception\*. STRATEGY 4 again: if any // complete pages were built before the failure, emit them rather than // discarding the work. $context = ['exception' => $e::class, 'pages' => $doc->getNumPages()]; if ($e instanceof ContextAwareExceptionInterface) { $context += $e->getContext(); } logRecovery('Unrecovered NextPDF failure; attempting a partial save', $context); if ($doc->getNumPages() > 0) { $doc->save($outputPath); }}
fwrite(STDERR, "Recovery pipeline complete.\n");STDOUT permanece libre para el arnés. Los diagnósticos de recuperación van a STDERR, y el PDF se escribe solo en NEXTPDF_COOKBOOK_OUTPUT.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- Ordenar los bloques catch de específico a general. PHP coincide con el primer
catchcompatible. Colocarcatch (NextPdfException $e)antes decatch (WriterException $e)convierte el bloque específico en código muerto, porqueWriterExceptionextiendeNextPdfException. DegradedExceptionqueda fuera de la jerarquía. ExtiendeRuntimeException, noNextPdfException. Una canalización que captura soloNextPdfExceptiondeja que un rechazo de política estricta se propague sin capturar. CapturarDegradedException(o unaRuntimeExceptionmás amplia) cuando haya una política de degradación no predeterminada activa.- Un respaldo de fuente también puede fallar. Si la propia tipografía de respaldo no está registrada, el segundo
setFont()vuelve a lanzar. Usar un alias de Base14 comohelvetica, que el motor resuelve sin una búsqueda en el sistema de archivos, o registrar una tipografía incluida medianteaddFontDirectory()durante el arranque para que el respaldo esté garantizado. getNumPages()cuenta la página activa sin vaciar. Devuelve el número de páginas vaciadas más uno cuando hay una página abierta actualmente. Por lo tanto, un «guardado parcial» incluye la página que se estaba construyendo cuando ocurrió el fallo, que suele ser lo deseado. Si solo se necesitan páginas completamente terminadas, también ramificar segúngetPage().- El respaldo con Chrome cambia la fidelidad, no solo la disponibilidad. La canalización en proceso y el puente con Chrome usan motores de maquetación distintos, así que un documento que recurre a Chrome puede verse diferente. Tratar el respaldo como una recuperación, no como un sustituto transparente, y registrar qué ruta produjo la salida.
- Un reintento debe usar una entrada de calidad conocida. El reintento con HTML simplificado solo ayuda cuando la variante simplificada es genuinamente más simple: menos selectores anidados, sin cadenas de
:has()que agoten el presupuesto de resolución. Reintentar con la misma entrada que ya falló produce un bucle hacia la misma excepción. - Inspeccionar las advertencias tras una ejecución limpia. Un renderizado que retorna sin lanzar todavía puede haberse degradado. Comprobar
hasDegradedParity()y leergetWarnings()antes de tratar la salida como fiel píxel a píxel; bajoDegradationPolicy::Permissivecada degradación es una advertencia, nunca una excepción.
Rendimiento
Sección titulada «Rendimiento»- La recuperación solo añade coste en la ruta de fallo. NextPDF lanza ante estados excepcionales, así que un renderizado limpio no incurre en coste por el
try/catchcircundante. - Un respaldo de renderer vuelve a ejecutar el renderizado. El intento en proceso se descarta y el intento con Chrome empieza de cero, así que un renderizado de respaldo cuesta, en el peor caso, ambos tiempos de renderizado más la ida y vuelta entre procesos hacia Chrome. Tenerlo en cuenta en el presupuesto al fijar los tiempos de espera de las solicitudes.
- Un reintento con HTML alternativo analiza un segundo documento. Mantener pequeña la variante simplificada para que el reintento sea barato en relación con el intento principal.
- Un guardado parcial serializa las páginas ya construidas. Su coste escala con el número de páginas que sobreviven, no con el trabajo que falló.
Notas de seguridad
Sección titulada «Notas de seguridad»- No exponer mensajes de excepción en bruto ni rutas del sistema de archivos a los usuarios finales. El mensaje de una
FontNotFoundExceptionincluye los directorios buscados y unaWriterExceptionincluye la ruta de salida; ambos filtran la disposición del servidor. Registrar el contexto estructurado del lado del servidor y devolver un mensaje genérico a quien hizo la llamada. - Tratar el HTML reintentado como entrada no confiable en cada intento. El respaldo y el reintento con HTML simplificado pasan ambos por el mismo límite de entrada; la canalización en proceso y el puente con Chrome aplican cada uno su propia política de seguridad de HTML, y un reintento no relaja esa validación. No asumir que una variante «simplificada» es más segura por el hecho de haberla escrito internamente.
- Un guardado parcial sigue escribiendo un archivo. Aplicar a una salida parcial las mismas reglas de validación de rutas, permisos y ubicación de almacenamiento que se aplican a una completa.
Document::save()rechaza los envoltorios de flujo y los bytes nulos y resuelve el directorio padre para bloquear el path traversal, pero el destino que se pasa sigue siendo responsabilidad de la aplicación.
Conformidad
Sección titulada «Conformidad»Esta recipe no formula ninguna afirmación normativa sobre estándares. Compone las API públicas de excepciones e inspección de documentos de NextPDF en un flujo de control de recuperación; no afirma comportamientos definidos por ISO 32000-2 ni ningún otro estándar, así que no lleva ningún bloque citations:.
Se verifica con el perfil de reproducibilidad semántica. El documento recuperado lleva un /ID en el tráiler y una fecha de modificación que se regeneran en cada guardado, así que no se puede alcanzar identidad de bytes. La comparación del AST estructural más los metadatos por sí solos es estable entre ejecuciones.
Consulta también
Sección titulada «Consulta también»- Maneja errores con la jerarquía de excepciones de NextPDF: granularidad de captura y contexto estructurado, la base sobre la que está construida esta página.
- Módulo de excepciones: la referencia completa de excepciones.
- Módulo de soporte:
DegradedException,Capability,Warning, y los tipos de degradación. - Módulo de configuración: configuración de la política de degradación.
- Renderiza PDF de forma segura en un worker de larga duración: recuperación en un worker que reutiliza registros compartidos.