Salta ai contenuti

Uso in produzione — fallback, telemetria, archiviazione e protezione

Questa pagina tratta quattro aspetti di produzione gestiti dal pacchetto oltre al semplice rendering: fallback locale, telemetria edge, archiviazione su R2 e livello di protezione delle API in ingresso. Ogni sezione descrive un comportamento verificato nelle classi.

Quando il Worker non è raggiungibile e fallbackToLocal è true, il bridge delega a un renderer locale. Fornire il renderer locale tramite LocalRendererFactoryInterface. Il bridge chiama la factory in modalità lazy, quindi il metodo create() della factory viene eseguito solo nel percorso di fallback.

<?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);
}
};
}
}

Collegare la factory al renderer:

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

Quando viene eseguito il fallback, il valore renderLocation del risultato è la stringa letterale local e heightPt è 0.0. Il percorso locale non riporta una posizione edge né un’altezza misurata. Il bridge passa al renderer locale la larghezza richiesta tramite la chiave di opzione widthPt.

La logica, letta direttamente da CloudflareHtmlRenderer, è la seguente:

SituazioneEsito
Configurazione incompleta, fallbackToLocal: falseCloudflareNotAvailableException
Configurazione incompleta, fallbackToLocal: true, factory collegataRendering locale
Il Worker genera un errore di trasporto, fallback abilitato, factory collegataRendering locale, con log a livello warning e poi info
Il Worker genera un’eccezione, fallback abilitato, Artisan installato, nessuna factoryCloudflareNotAvailableException che indica la factory mancante
Il Worker genera un’eccezione, fallback abilitato, Artisan non installatoCloudflareNotAvailableException che indica il pacchetto mancante
Il Worker restituisce un errore HTTP / un corpo malformatoCloudflareRenderException, non ricorre mai al fallback

L’ultima riga rappresenta la distinzione fondamentale. Un Worker che risponde con un errore costituisce un guasto del rendering, non un problema di raggiungibilità. L’errore viene rilanciato, in modo che il codice possa distinguere un rendering interrotto da un edge irraggiungibile.

Ogni rendering riuscito nel percorso binario include telemetria derivata dagli header della risposta:

$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(),
]);

Il renderer legge renderLocation dall’header di risposta CF-Ray, usando il segmento dopo il trattino finale. Per CF-Ray: 8abc123def456-TPE la posizione è TPE. Quando l’header è assente, la posizione è una stringa vuota. Nel percorso di risposta JSON il valore proviene invece dal campo JSON renderLocation. Considerare questi valori come segnali di osservabilità emessi dal Worker, non come garanzie della piattaforma.

R2ArchiveManager carica i byte del PDF su Cloudflare R2 tramite l’API compatibile con S3, firmando le richieste 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]);
}

Comportamento verificato in R2ArchiveManager e R2ObjectKey:

  • La chiave dell’oggetto è partizionata per data: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, ad esempio invoices/2026/05/18/invoice-2026-0042.pdf.
  • Il nome del file viene sanificato: viene applicato basename() (rimozione del path traversal), quindi vengono eliminati i byte nulli e i caratteri di controllo (\x00\x1f, \x7f). Se il risultato è vuoto, diventa document.pdf.
  • I metadati personalizzati vengono inviati come header x-amz-meta-<lowercased-key>, inclusi nell’insieme degli header firmati V4.
  • Un upload superiore a maxFileSizeBytes (predefinito 104857600) viene rifiutato prima di qualsiasi richiesta, restituendo un R2UploadResult con success: false.
  • R2UploadResult::isValid() richiede success, una key non vuota e un etag non vuoto.
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() costruisce un URL GET firmato in query con AWS Signature V4 e un X-Amz-Expires configurabile (predefinito 3600 secondi). La richiesta canonica utilizza il valore sentinella dell’hash del contenuto UNSIGNED-PAYLOAD. Un URL di lettura firmato in query utilizza questa forma perché il corpo non fa parte della richiesta firmata. Questo descrive il comportamento di firma implementato dal pacchetto, come risulta da R2ArchiveManager. La documentazione del servizio Amazon definisce AWS Signature Version 4, non uno standard SDO, perciò qui non viene citata alcuna clausola normativa. Le chiavi di accesso agli oggetti sono #[SensitiveParameter]; tenerle fuori dai log.

R2UploadResult::publicUrl($customDomain) restituisce la sola chiave quando non viene fornito alcun dominio, oppure https://<domain>/<key> in caso contrario. Forza uno schema HTTPS quando il dominio fornito ne è privo. Non rende pubblico un bucket privato. È un aspetto della configurazione del bucket R2.

ApiProtection è il livello da applicare alle richieste di rendering che arrivano a un gateway PHP davanti al Worker. Esegue tre controlli in un ordine fisso: chiave API, quindi dimensione del payload, quindi limite di frequenza.

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;
}

Comportamento verificato:

  • L’ordine è chiave API → dimensione del payload → limite di frequenza. Il primo controllo che fallisce interrompe subito la valutazione con un denialReason specifico.
  • ApiKeyValidator::validate() usa hash_equals() per un confronto a tempo costante e rifiuta una chiave vuota. validateHashed() esegue il confronto con hash SHA-256 per archiviare le chiavi a riposo. I parametri delle chiavi recano #[SensitiveParameter].
  • Lo store del limite di frequenza è in memoria per processo. Tiene traccia di una finestra al minuto (rateLimitWindowSeconds, predefinito 60) e di una finestra all’ora (fissa a 3600 secondi). Non persiste tra i worker né tra i riavvii. Per applicare un limite condiviso tra processi, anteporre uno store condiviso.
  • ApiProtectionResult::toHeaders() aggiunge sempre X-Content-Type-Options: nosniff e X-Frame-Options: DENY, e unisce gli header del limite di frequenza (X-RateLimit-Remaining, X-RateLimit-Reset, oltre a Retry-After quando la richiesta viene negata).

Questo bridge non firma i PDF. Una pipeline di firma di produzione esegue il rendering all’edge, quindi firma i byte restituiti con il motore scelto:

  1. render()CloudflareRenderResult::$pdfData.
  2. Passare $pdfData a nextpdf/core (oppure NextPDF Pro per la firma PAdES B-B). I profili di validazione a lungo termine sono una funzionalità Enterprise; questo bridge core non dichiara il supporto di nessuna delle due funzionalità.

Mantenere la fase di firma nel proprio processo, in modo che la chiave di firma non oltrepassi mai il confine dell’edge.

  • /integrations/cloudflare/security-and-operations/ — pinning, difesa SSRF, rotazione dei segreti, runbook operativo.
  • /integrations/cloudflare/troubleshooting/ — catalogo delle modalità di errore.
  • /integrations/cloudflare/configuration/ — ogni campo e valore predefinito.