Ir al contenido

Uso en producción: respaldo, telemetría, archivado y protección

Esta página cubre cuatro aspectos de producción que el paquete gestiona más allá de un renderizado básico: respaldo local, telemetría de borde, archivado en R2 y la capa de protección de la API entrante. Cada sección se corresponde con un comportamiento verificado en la clase correspondiente.

Cuando no se puede acceder al Worker y fallbackToLocal es true, el puente delega en un renderizador local. Ese renderizador local se proporciona mediante LocalRendererFactoryInterface. El puente llama a la factoría de forma diferida, por lo que el create() de la factoría solo se ejecuta en la ruta de respaldo.

<?php
declare(strict_types=1);
use NextPDF\Cloudflare\Contract\LocalRendererFactoryInterface;
use NextPDF\Cloudflare\Contract\LocalRendererInterface;
final class ArtisanLocalRendererFactory implements LocalRendererFactoryInterface
{
public function __construct(
private readonly \NextPDF\Artisan\ChromeHtmlRenderer $chrome,
) {}
public function create(): LocalRendererInterface
{
return new readonly class($this->chrome) implements LocalRendererInterface {
public function __construct(
private \NextPDF\Artisan\ChromeHtmlRenderer $chrome,
) {}
/** @param array<string, mixed> $options */
public function render(string $html, array $options = []): string
{
// Delegate to the local Chrome renderer; return raw PDF bytes.
return $this->chrome->renderToString($html, $options);
}
};
}
}

Conectar la factoría al renderizador:

use NextPDF\Cloudflare\CloudflareHtmlRenderer;
$renderer = new CloudflareHtmlRenderer(
config: $config,
httpClient: $httpClient,
requestFactory: $httpFactory,
streamFactory: $httpFactory,
logger: $logger,
localRendererFactory: new ArtisanLocalRendererFactory($chrome),
responseFactory: $httpFactory,
);

Cuando se ejecuta el respaldo, el renderLocation del resultado es la cadena literal local y heightPt es 0.0. La ruta local no informa de ninguna ubicación de borde ni de una altura medida. El puente pasa el ancho solicitado al renderizador local mediante la clave de opción widthPt.

Leído directamente desde CloudflareHtmlRenderer:

SituaciónResultado
Configuración incompleta, fallbackToLocal: falseCloudflareNotAvailableException
Configuración incompleta, fallbackToLocal: true, factoría configuradaRenderizado local
El Worker lanza un error de transporte, respaldo habilitado, factoría configuradaRenderizado local, registrado en warning y luego en info
El Worker lanza una excepción, respaldo habilitado, Artisan instalado, sin factoríaCloudflareNotAvailableException que nombra la factoría ausente
El Worker lanza una excepción, respaldo habilitado, Artisan no instaladoCloudflareNotAvailableException que nombra el paquete ausente
El Worker devuelve un error HTTP / cuerpo mal formadoCloudflareRenderException, nunca recurre al respaldo

La última fila marca la distinción crítica. Un Worker que responde con un error es un fallo de renderizado, no un fallo de accesibilidad. Se vuelve a lanzar para que el código pueda distinguir un renderizado roto de un borde inaccesible.

Cada renderizado correcto que pasa por la ruta binaria incluye telemetría derivada de las cabeceras de respuesta:

$result = $renderer->render($html);
$logger->info('edge render', [
'edge' => $result->renderLocation, // e.g. 'TPE', 'NRT'
'render_time_ms' => $result->renderTimeMs,
'content_px' => $result->contentHeightPx,
'pdf_bytes' => $result->size(),
]);

El renderizador lee renderLocation de la cabecera de respuesta CF-Ray y toma el segmento posterior al último guion. Para CF-Ray: 8abc123def456-TPE la ubicación es TPE. Cuando la cabecera está ausente, la ubicación es una cadena vacía. En la ruta de respuesta JSON, en cambio, el valor proviene del campo JSON renderLocation. Estos valores deben tratarse como señales de observabilidad del Worker, no como garantías de la plataforma.

R2ArchiveManager sube los bytes del PDF a Cloudflare R2 mediante la API compatible con S3 y firma las solicitudes con AWS Signature V4.

use NextPDF\Cloudflare\R2ArchiveConfig;
use NextPDF\Cloudflare\R2ArchiveManager;
$r2 = new R2ArchiveManager(
config: new R2ArchiveConfig(
bucketName: 'pdf-archive',
accountId: getenv('CF_ACCOUNT_ID') ?: '',
accessKeyId: getenv('R2_ACCESS_KEY_ID') ?: '',
secretAccessKey: getenv('R2_SECRET_ACCESS_KEY') ?: '',
pathPrefix: 'invoices/',
),
httpClient: $httpClient,
requestFactory: $httpFactory,
streamFactory: $httpFactory,
);
$upload = $r2->upload($result->pdfData, 'invoice-2026-0042.pdf', [
'tenant' => 'acme',
]);
if (!$upload->success) {
$logger->error('r2 upload failed', ['error' => $upload->error]);
}

Comportamiento verificado en R2ArchiveManager y R2ObjectKey:

  • La clave del objeto se particiona por fecha: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, por ejemplo invoices/2026/05/18/invoice-2026-0042.pdf.
  • El nombre de archivo se sanea: se aplica basename() (eliminación del path traversal) y, después, se eliminan los bytes nulos y los caracteres de control (\x00\x1f, \x7f). Un resultado vacío se convierte en document.pdf.
  • Los metadatos personalizados se envían como cabeceras x-amz-meta-<lowercased-key>, incluidas en el conjunto de cabeceras firmadas de V4.
  • Una subida mayor que maxFileSizeBytes (por defecto 104857600) se rechaza antes de realizar cualquier solicitud y devuelve un R2UploadResult con success: false.
  • R2UploadResult::isValid() requiere success, una key no vacía y un etag no vacío.
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() construye una URL GET con firma en la consulta mediante AWS Signature V4 y con un X-Amz-Expires controlado por quien llama (por defecto 3600 segundos). La solicitud canónica usa el centinela de hash de contenido UNSIGNED-PAYLOAD. Una URL de lectura con firma en la consulta usa esa forma porque el cuerpo no forma parte de la solicitud firmada. Esto describe el comportamiento de firma implementado por el paquete, tal como se desprende de R2ArchiveManager. La documentación del servicio de Amazon define AWS Signature Version 4, no un estándar de un SDO, por lo que aquí no se fija ninguna cláusula normativa. Las claves de acceso a los objetos son #[SensitiveParameter]; deben mantenerse fuera de los registros.

R2UploadResult::publicUrl($customDomain) devuelve únicamente la clave cuando no se indica ningún dominio, o https://<domain>/<key> en caso contrario. Fuerza un esquema HTTPS cuando el dominio proporcionado no tiene ninguno. No convierte un bucket privado en público; eso depende de la configuración del bucket de R2.

ApiProtection es la capa que se aplica a las solicitudes de renderizado que llegan a una pasarela PHP situada delante del Worker. Ejecuta tres comprobaciones en un orden fijo: clave de API, después tamaño de la carga útil y, por último, límite de tasa.

use NextPDF\Cloudflare\ApiKeyValidator;
use NextPDF\Cloudflare\ApiProtection;
use NextPDF\Cloudflare\ApiProtectionConfig;
$protection = new ApiProtection(
config: new ApiProtectionConfig(
maxRequestsPerMinute: 30,
maxRequestsPerHour: 500,
maxPayloadSizeBytes: 5_000_000,
requireApiKey: true,
),
keyValidator: new ApiKeyValidator([getenv('GATEWAY_API_KEY') ?: '']),
);
$decision = $protection->checkRequest(
clientId: $clientIp,
payloadSize: strlen($requestBody),
apiKey: $request->getHeaderLine('X-Api-Key'),
);
if (!$decision->allowed) {
http_response_code(429);
foreach ($decision->toHeaders() as $name => $value) {
header("{$name}: {$value}");
}
echo $decision->denialReason;
exit;
}

Comportamiento verificado:

  • El orden es clave de API → tamaño de la carga útil → límite de tasa. La primera comprobación fallida hace cortocircuito con un denialReason específico.
  • ApiKeyValidator::validate() usa hash_equals() para una comparación segura contra ataques de temporización y rechaza una clave vacía. validateHashed() compara contra hashes SHA-256 para el almacenamiento de claves en reposo. Los parámetros de clave llevan #[SensitiveParameter].
  • El almacén de límite de tasa es en memoria por proceso. Rastrea una ventana por minuto (rateLimitWindowSeconds, por defecto 60) y una ventana por hora (fija en 3600 segundos). No persiste entre workers ni reinicios. Para aplicar un límite compartido entre procesos, se debe anteponer un almacén compartido.
  • ApiProtectionResult::toHeaders() siempre añade X-Content-Type-Options: nosniff y X-Frame-Options: DENY, y combina las cabeceras de límite de tasa (X-RateLimit-Remaining, X-RateLimit-Reset, más Retry-After cuando se deniega).

Este puente no firma archivos PDF. Una canalización de firma de producción renderiza en el borde y luego firma los bytes devueltos con el motor:

  1. render()CloudflareRenderResult::$pdfData.
  2. Entregar $pdfData a nextpdf/core (o NextPDF Pro para la firma PAdES B-B). Los perfiles de validación a largo plazo son una capacidad Enterprise; este puente core no reivindica ninguna de esas capacidades.

Mantener el paso de firma en su propio proceso para que la clave de firma nunca cruce el límite del borde.

  • /integrations/cloudflare/security-and-operations/ — fijado (pinning), defensa frente a SSRF, rotación de secretos, manual operativo.
  • /integrations/cloudflare/troubleshooting/ — catálogo de modos de fallo.
  • /integrations/cloudflare/configuration/ — cada campo y su valor por defecto.