Rendering all'edge con Cloudflare e fallback locale
In breve
Sezione intitolata “In breve”Il bridge Cloudflare invia l’HTML a un endpoint Cloudflare Worker di rendering e restituisce il PDF. Il rendering avviene all’edge, senza dover gestire processi browser a lunga esecuzione. Si prepara una configurazione solo HTTPS, si collegano un client PSR-18 e le factory PSR-17, si chiama render() e, se necessario, si collega un renderer locale che il bridge usa come fallback quando il Worker non è raggiungibile. Questa guida mostra la chiamata di rendering, la scelta del fallback e i controlli SSRF, anti-DNS-rebinding e di pinning della chiave pubblica TLS che il bridge impone prima che qualunque richiesta esca dal processo.
Prerequisiti da predisporre:
- Il core di NextPDF e
nextpdf/cloudflaresono installati. - Un endpoint Worker espone il contratto di rendering su HTTPS e accetta un bearer token. Il bridge rifiuta un URL del Worker non HTTPS prima di inviare qualsiasi dato.
- Un client PSR-18 (ad esempio Guzzle 7) e le factory PSR-17 per le richieste e i flussi sono disponibili nel percorso di esecuzione. Per il trasporto cURL con pinning, fornire anche una factory di risposta PSR-17 ed
ext-curl. - Per il fallback locale, è disponibile
nextpdf/artisan(o un altro renderer locale).
Questa guida è operativa. Per un primo rendering eseguibile, consultare la guida rapida di Cloudflare.
Installazione
Sezione intitolata “Installazione”Installare il bridge, un client PSR-18 e le factory PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzlePer il fallback locale, installare un renderer locale a cui il bridge possa delegare.
composer require nextpdf/artisanRecuperare il bearer token del Worker ed eventuali credenziali R2 da variabili d’ambiente o da un gestore di segreti. Non eseguirne mai il commit.
Panoramica concettuale
Sezione intitolata “Panoramica concettuale”CloudflareHtmlRenderer::render() convalida l’HTML e la destinazione, invia al Worker un POST autenticato e interpreta la risposta. Il Worker restituisce byte PDF grezzi (Content-Type: application/pdf) oppure un corpo JSON con un campo pdf codificato in base64. Il renderer mappa il risultato in un final readonly CloudflareRenderResult che contiene i byte, la larghezza richiesta, l’altezza, la posizione di rendering (derivata dall’header CF-Ray) e il tempo di rendering.
Il bridge distingue in modo esplicito due classi di errore:
CloudflareRenderException— il Worker ha risposto, ma il rendering non è riuscito (un errore HTTP o un corpo che non inizia con%PDF). Si tratta di un errore di rendering e non viene mai ritentato con un fallback.CloudflareNotAvailableException— non è stato possibile raggiungere l’edge e non era disponibile alcun fallback utilizzabile.
Il fallback locale copre il secondo caso. Quando il Worker non può essere raggiunto e fallbackToLocal è true, il bridge richiama una LocalRendererFactoryInterface fornita dall’utente, e lo fa in modo lazy: il metodo create() della factory viene eseguito solo nel percorso di fallback. Durante un rendering di fallback, il valore renderLocation del risultato è la stringa letterale local.
Il bridge protegge il confine di rete prima che qualunque richiesta lasci PHP. Rifiuta un URL del Worker non HTTPS. Rifiuta un host del Worker che si risolve in uno spazio di indirizzi privato o riservato, controllando tutti i record A e AAAA anziché solo il primo. Inoltre, risolve nuovamente l’host immediatamente prima di connettersi, chiudendo la finestra time-of-check/time-of-use contro il DNS rebinding. Quando vengono forniti una factory di risposta PSR-17 e un set di IP risolti oppure dei pin SPKI, il bridge utilizza un trasporto cURL con pinning. Tale trasporto vincola la connessione agli IP verificati (CURLOPT_RESOLVE), impone il pinning della chiave pubblica TLS (CURLOPT_PINNEDPUBLICKEY), verifica il peer e l’host e non segue i redirect.
Superficie API
Sezione intitolata “Superficie API”// Configuration (final readonly):new CloudflareRendererConfig( string $workerUrl, // required, must be HTTPS string $apiToken, // required, #[SensitiveParameter] int $renderTimeout = 30, string $defaultCss = '', int $maxHtmlSize = 5_000_000, ?string $r2FontBucket = null, bool $fallbackToLocal = true, list<string> $pinnedPublicKeys = [], // sha256/<base64> list<string> $backupPublicKeys = [],)CloudflareRendererConfig::fromArray(array $config): self
// The renderer:new CloudflareHtmlRenderer( CloudflareRendererConfig $config, ClientInterface $httpClient, // PSR-18 RequestFactoryInterface $requestFactory, // PSR-17 StreamFactoryInterface $streamFactory, // PSR-17 ?LoggerInterface $logger = null, // PSR-3 ?LocalRendererFactoryInterface $localRendererFactory = null, ?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null, ?ResponseFactoryInterface $responseFactory = null, // enables pinned transport)CloudflareHtmlRenderer::render(string $html, float $widthPt = 595.28, float $heightPt = 0.0, list<string> $fontFiles = []): CloudflareRenderResultCloudflareHtmlRenderer::isAvailable(): boolrender() usa per impostazione predefinita la larghezza A4 (595.28 punti) e il rilevamento automatico dell’altezza (heightPt: 0). Per il riferimento completo dei campi e la mappa delle chiavi di fromArray(), consultare la pagina di configurazione di Cloudflare collegata in Vedere anche.
Esempio di codice — Avvio rapido
Sezione intitolata “Esempio di codice — Avvio rapido”Creare la configurazione, inizializzare il renderer, eseguire il rendering e scrivere i byte.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;use GuzzleHttp\Psr7\HttpFactory;use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;use NextPDF\Cloudflare\Exception\CloudflareNotAvailableException;use NextPDF\Cloudflare\Exception\CloudflareRenderException;
$config = new CloudflareRendererConfig( workerUrl: 'https://pdf-renderer.example.workers.dev/render', apiToken: getenv('CF_PDF_TOKEN') ?: throw new RuntimeException('CF_PDF_TOKEN not set'),);
$httpFactory = new HttpFactory();
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: new Client(), requestFactory: $httpFactory, streamFactory: $httpFactory, responseFactory: $httpFactory, // enables the pinned cURL transport);
try { $result = $renderer->render('<h1>Hello from the edge</h1>');
if (!$result->isValid()) { throw new RuntimeException('Worker did not return a valid PDF'); }
file_put_contents('output.pdf', $result->pdfData);} catch (CloudflareRenderException $exception) { // Worker answered but the render failed. Not retried with fallback. fwrite(STDERR, 'Render failed: ' . $exception->getMessage() . PHP_EOL); exit(1);} catch (CloudflareNotAvailableException $exception) { // Edge unreachable and no usable fallback. fwrite(STDERR, 'Edge unavailable: ' . $exception->getMessage() . PHP_EOL); exit(2);}Il token viene letto dall’ambiente, mai inserito direttamente nel codice. workerUrl deve essere HTTPS; il bridge rifiuta un URL http:// prima di inviare qualsiasi richiesta.
Esempio di codice — Produzione
Sezione intitolata “Esempio di codice — Produzione”In produzione, collegare una factory di renderer locale in modo che un Worker non raggiungibile attivi il fallback invece di far fallire la richiesta, e configurare i pin TLS con un pin di backup. Il metodo create() della factory viene eseguito solo nel percorso di fallback.
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;use NextPDF\Cloudflare\Contract\LocalRendererFactoryInterface;use NextPDF\Cloudflare\Contract\LocalRendererInterface;
final readonly class ArtisanLocalRendererFactory implements LocalRendererFactoryInterface{ public function __construct(private ChromeHtmlRenderer $chrome) {}
public function create(): LocalRendererInterface { return new readonly class($this->chrome) implements LocalRendererInterface { public function __construct(private ChromeHtmlRenderer $chrome) {}
/** @param array<string, mixed> $options */ public function render(string $html, array $options = []): string { $widthPt = (float) ($options['widthPt'] ?? 595.28); // A4 width $heightPt = (float) ($options['heightPt'] ?? 0.0); // 0 = auto-fit
return $this->chrome->render($html, $widthPt, $heightPt)->getPdfData(); } }; }}Collegare la factory e i pin al renderer.
<?php
declare(strict_types=1);
use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;
$config = CloudflareRendererConfig::fromArray([ 'worker_url' => getenv('CF_WORKER_URL') ?: '', 'api_token' => getenv('CF_PDF_TOKEN') ?: '', 'render_timeout' => 60, 'fallback_to_local' => true, 'pinned_public_keys' => ['sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg='], 'backup_public_keys' => ['sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys='],]);
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory, logger: $logger, localRendererFactory: new ArtisanLocalRendererFactory($chrome), responseFactory: $httpFactory,);Quando il fallback viene eseguito, il valore renderLocation del risultato è local e heightPt è 0.0. Il bridge registra il fallback a livello warning e poi info. Configurare sempre un pin di backup prima di una rotazione del certificato, in modo che una rotazione pianificata non blocchi l’accesso del bridge all’endpoint.
Casi limite e insidie
Sezione intitolata “Casi limite e insidie”- Un errore del Worker non è un errore di raggiungibilità. Un Worker che risponde con un errore HTTP o un corpo malformato solleva
CloudflareRenderExceptione non viene mai ritentato con il fallback. Solo un edge non raggiungibile attiva il fallback. Mantenere separati i due rami catch. - Il fallback richiede sia il flag sia una factory. Con
fallbackToLocal: truema senza una factory collegata, un Worker non raggiungibile sollevaCloudflareNotAvailableExceptionche indica il nome della factory mancante. Collegare la factory. isAvailable()è un segnale, non una garanzia. Invia unHEADautenticato e restituiscetrueper uno stato inferiore a500; ilPOSTsuccessivo può comunque fallire. Non considerarlo un contratto.- Il pinning è opt-in. Un set di pin vuoto disabilita il pinning. Utilizzare un set vuoto solo con una catena di certificati stabile e nota e, una volta attivato il pinning, mantenere un pin di backup.
fontFilesrichiede un bucket R2. L’argomentofontFilesè rilevante solo quando la configurazione impostar2FontBucket; in caso contrario non ha alcun effetto.- Il bridge non firma. Restituisce byte PDF. Eseguire il rendering all’edge, quindi firmare nel proprio processo, in modo che la chiave di firma non oltrepassi mai il confine dell’edge.
Prestazioni
Sezione intitolata “Prestazioni”Il rendering all’edge sposta interamente il costo del browser fuori dai propri host. Il costo residuo è un round-trip HTTPS verso il Worker più il tempo di rendering del Worker stesso, che il risultato riporta come renderTimeMs. Il bridge applica il timeout configurato attraverso il trasporto con pinning. Impostarlo in base alla latenza misurata del Worker con un margine di sicurezza e mantenerlo al di sotto di qualsiasi timeout del gateway a monte. Il pacchetto dichiara solo i limiti che impone direttamente. Non formula alcuna affermazione sui limiti massimi di CPU, memoria o corpo della richiesta della piattaforma Cloudflare. Per questi, consultare la documentazione di Cloudflare e il proprio Worker.
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”- La destinazione viene convalidata prima che la richiesta lasci PHP. Gli URL non HTTPS vengono rifiutati. Un host che si risolve in uno spazio di indirizzi privato o riservato viene rifiutato su tutti i record A e AAAA. L’host viene risolto nuovamente immediatamente prima di connettersi, per difendersi dal DNS rebinding.
- Il trasporto con pinning vincola DNS e TLS. Con una factory di risposta e i pin configurati, il bridge vincola la connessione agli IP verificati, impone il pinning SPKI, verifica il peer e l’host e rifiuta di seguire redirect verso un host non verificato.
- L’input è limitato. L’HTML che supera
maxHtmlSize(5 MB per impostazione predefinita), un data URI base64 sovradimensionato e qualsiasi tag<meta http-equiv="refresh">vengono rifiutati prima dell’invio della richiesta. - I segreti sono offuscati e immutabili.
apiTokene le chiavi R2 sono contrassegnati con#[SensitiveParameter], quindi vengono offuscati dagli stack trace, e gli oggetti di configurazione sonofinal readonly. Recuperare i segreti dall’ambiente o da un gestore di segreti; non eseguirne mai il commit. - Non scrivere mai un blocco
catchvuoto. Ogni esempio intercetta il tipo di eccezione specifico e registra un log o termina con un codice definito.
Il modello di sicurezza completo — la difesa da SSRF e DNS rebinding, la guida operativa al pinning e l’approccio alla gestione dei segreti — è descritto nella pagina Cloudflare security-and-operations collegata in Vedere anche, dove sono riportate le clausole OWASP e RFC 7469 pertinenti.
Conformità
Sezione intitolata “Conformità”Questa guida non formula alcuna affermazione normativa propria sugli standard. Nelle pagine a monte Cloudflare security-and-operations e di configurazione, la risoluzione DNS su tutti i record del bridge e il ricontrollo TOCTOU sono mappati alle linee guida OWASP per la prevenzione dell’SSRF, mentre il pinning della chiave pubblica TLS e il ripristino tramite pin di backup sono mappati a RFC 7469. Questa pagina del cookbook ribadisce l’utilizzo e rimanda per tali citazioni a quelle pagine. Il bridge non esegue alcuna firma e non formula alcuna affermazione di conformità delle firme.
Vedere anche
Sezione intitolata “Vedere anche”- Eseguire il rendering di HTML in PDF con il renderer Chrome di Artisan — il renderer in-process utilizzato qui come fallback locale.
- Guida rapida di Cloudflare — il primo rendering all’edge e il modello del risultato.
- Sicurezza e operazioni di Cloudflare — SSRF, DNS rebinding, pinning e rotazione dei segreti.
- Utilizzo in produzione di Cloudflare — collegamento del fallback, telemetria, archiviazione su R2 e protezione delle API.