Effectuer le rendu en périphérie avec Cloudflare, avec repli local
Le pont Cloudflare envoie ton HTML à un endpoint de rendu Cloudflare Worker et renvoie le PDF. Le rendu s’exécute en périphérie, sans processus de navigateur de longue durée à gérer. Tu construis une configuration exclusivement HTTPS, câbles un client PSR-18 et des fabriques PSR-17, appelles render(), puis câbles éventuellement un renderer local vers lequel le pont se replie quand le Worker est injoignable. Ce guide couvre l’appel de rendu, la décision de repli et les contrôles SSRF, anti-DNS-rebinding et d’épinglage de clé publique TLS que le pont impose avant qu’aucune requête ne quitte le processus.
Les prérequis, d’emblée :
- Le cœur NextPDF et
nextpdf/cloudflaresont installés. - Un endpoint Worker sert le contrat de rendu en HTTPS et accepte un jeton bearer. Le pont rejette toute URL de Worker non HTTPS avant d’envoyer quoi que ce soit.
- Un client PSR-18 (par exemple Guzzle 7) ainsi que des fabriques de requête et de flux PSR-17 sont disponibles dans le chemin de classes. Pour le transport cURL épinglé, fournis aussi une fabrique de réponse PSR-17 et
ext-curl. - Pour le repli local,
nextpdf/artisan(ou un autre renderer local) est disponible.
Ceci est un guide pratique. Pour un premier rendu exécutable, lis le démarrage rapide Cloudflare.
Installation
Section intitulée « Installation »Installe le pont, un client PSR-18 et des fabriques PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzlePour le repli local, installe un renderer local auquel le pont pourra déléguer.
composer require nextpdf/artisanRécupère le jeton bearer du Worker et les éventuels identifiants R2 depuis des variables d’environnement ou un gestionnaire de secrets. Ne les commite jamais.
Vue d’ensemble conceptuelle
Section intitulée « Vue d’ensemble conceptuelle »CloudflareHtmlRenderer::render() valide le HTML et la destination, envoie un POST authentifié au Worker, puis analyse la réponse. Le Worker renvoie soit des octets PDF bruts (Content-Type: application/pdf), soit un corps JSON contenant un champ pdf encodé en base64. Le renderer mappe le résultat vers un final readonly CloudflareRenderResult qui contient les octets, la largeur demandée, la hauteur, l’emplacement de rendu (dérivé de l’en-tête CF-Ray) et le temps de rendu.
Le pont distingue volontairement deux classes d’échec :
CloudflareRenderException— le Worker a répondu, mais le rendu a échoué (une erreur HTTP ou un corps qui ne commence pas par%PDF). C’est un échec de rendu et il n’est jamais retenté avec un repli.CloudflareNotAvailableException— la périphérie n’a pas pu être jointe et aucun repli utilisable n’était disponible.
Le repli local couvre ce second cas. Quand le Worker ne peut pas être joint et que fallbackToLocal vaut true, le pont appelle un LocalRendererFactoryInterface que tu fournis, et il le fait paresseusement : le create() de la fabrique ne s’exécute que sur le chemin de repli. Lors d’un rendu de repli, le renderLocation du résultat est la chaîne littérale local.
Le pont défend la frontière réseau avant qu’aucune requête ne quitte PHP. Il rejette toute URL de Worker non HTTPS. Il rejette un hôte de Worker qui se résout vers un espace d’adressage privé ou réservé en vérifiant tous les enregistrements A et AAAA, pas seulement le premier. Il résout aussi l’hôte à nouveau juste avant de se connecter, ce qui ferme la fenêtre time-of-check/time-of-use contre le DNS rebinding. Quand tu fournis une fabrique de réponse PSR-17 ainsi qu’un ensemble d’IP résolues ou des épingles SPKI, le pont utilise un transport cURL épinglé. Ce transport lie la connexion aux IP vérifiées (CURLOPT_RESOLVE), impose l’épinglage de clé publique TLS (CURLOPT_PINNEDPUBLICKEY), vérifie le pair et l’hôte, et ne suit pas les redirections.
Surface d’API
Section intitulée « Surface d’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() utilise par défaut la largeur A4 (595.28 points) et une hauteur détectée automatiquement (heightPt: 0). Pour la référence complète des champs et le tableau des clés de fromArray(), consulte la page de configuration Cloudflare liée sous Voir aussi.
Exemple de code — Démarrage rapide
Section intitulée « Exemple de code — Démarrage rapide »Construis la configuration, instancie le renderer, lance le rendu et écris les octets.
<?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);}Le jeton est lu depuis l’environnement, jamais codé en dur. workerUrl doit être en HTTPS ; le pont rejette une URL http:// avant d’envoyer la moindre requête.
Exemple de code — Production
Section intitulée « Exemple de code — Production »En production, câble une fabrique de renderer local pour qu’un Worker injoignable déclenche un repli au lieu de faire échouer la requête, et configure les épingles TLS avec une épingle de secours. Le create() de la fabrique ne s’exécute que sur le chemin de repli.
<?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(); } }; }}Câble la fabrique et les épingles dans le 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,);Quand le repli s’exécute, le renderLocation du résultat vaut local et heightPt vaut 0.0. Le pont journalise le repli en warning, puis en info. Configure toujours une épingle de secours avant une rotation de certificat, afin qu’une rotation planifiée ne coupe pas le pont de l’endpoint.
Cas limites & pièges
Section intitulée « Cas limites & pièges »- Une erreur du Worker n’est pas un échec d’accessibilité. Un Worker qui répond avec une erreur HTTP ou un corps malformé lève
CloudflareRenderExceptionet n’est jamais retenté avec le repli. Seule une périphérie injoignable déclenche le repli. Garde les deux branches catch distinctes. - Le repli nécessite à la fois le drapeau et une fabrique. Avec
fallbackToLocal: truemais sans fabrique câblée, un Worker injoignable lèveCloudflareNotAvailableExceptionqui indique la fabrique manquante. Câble la fabrique. isAvailable()est un indice, pas une garantie. Il envoie unHEADauthentifié et renvoietruepour un statut inférieur à500; lePOSTqui suit peut quand même échouer. Ne le traite pas comme un contrat.- L’épinglage est optionnel. Un ensemble d’épingles vide désactive l’épinglage. N’utilise un ensemble vide qu’avec une chaîne de certificats stable et connue, et garde une épingle de secours dès que tu actives l’épinglage.
fontFilesnécessite un bucket R2. L’argumentfontFilesn’est pris en compte que lorsque la configuration définitr2FontBucket; sinon il n’a aucun effet.- Le pont ne signe pas. Il renvoie des octets PDF. Fais le rendu en périphérie, puis signe dans ton propre processus afin que la clé de signature ne franchisse jamais la frontière de la périphérie.
Performance
Section intitulée « Performance »Le rendu en périphérie déplace entièrement le coût du navigateur hors de tes hôtes. Ce que tu paies à la place, c’est un aller-retour HTTPS vers le Worker plus le temps de rendu propre au Worker, que le résultat expose comme renderTimeMs. Le pont applique le délai d’expiration configuré via le transport épinglé. Règle-le à partir de la latence mesurée du Worker, avec une marge, et garde-le inférieur à tout délai d’expiration de passerelle en amont. Le package n’énonce que les limites qu’il impose lui-même. Il ne fait aucune affirmation sur les plafonds de CPU, de mémoire ou de corps de requête de la plateforme Cloudflare. Pour ceux-là, consulte la documentation de Cloudflare et ton Worker.
Notes de sécurité
Section intitulée « Notes de sécurité »- La destination est validée avant que la requête ne quitte PHP. Les URL non HTTPS sont rejetées. Un hôte qui se résout vers un espace d’adressage privé ou réservé est rejeté sur l’ensemble des enregistrements A et AAAA. L’hôte est résolu à nouveau juste avant de se connecter, pour se défendre contre le DNS rebinding.
- Le transport épinglé lie le DNS et le TLS. Avec une fabrique de réponse et des épingles configurées, le pont lie la connexion aux IP vérifiées, impose l’épinglage SPKI, vérifie le pair et l’hôte, et refuse de suivre les redirections vers un hôte non vérifié.
- L’entrée est bornée. Un HTML dépassant
maxHtmlSize(5 Mo par défaut), une URI de données base64 surdimensionnée et toute balise<meta http-equiv="refresh">sont rejetés avant l’envoi de la requête. - Les secrets sont expurgés et immuables.
apiTokenet les clés R2 portent#[SensitiveParameter], donc ils sont expurgés des traces de pile, et les objets de configuration sontfinal readonly. Récupère les secrets depuis l’environnement ou un gestionnaire de secrets ; ne les commite jamais. - N’écris jamais un bloc
catchvide. Chaque exemple intercepte le type d’exception spécifique et journalise ou sort avec un code défini.
Le modèle de sécurité complet — la défense SSRF et anti-DNS-rebinding, le guide opérationnel d’épinglage et la posture de gestion des secrets — se trouve sur la page sécurité et opérations Cloudflare liée sous Voir aussi, avec les clauses OWASP et RFC 7469 pertinentes.
Conformité
Section intitulée « Conformité »Ce guide ne formule de lui-même aucune affirmation normative de conformité à une norme. Dans les pages amont Cloudflare sécurité et opérations et configuration, la résolution DNS du pont sur tous les enregistrements et sa re-vérification TOCTOU sont mappées au guide de prévention SSRF de l’OWASP, et son épinglage de clé publique TLS ainsi que sa récupération par épingle de secours sont mappés au RFC 7469. Cette page du Cookbook réénonce l’usage et s’en remet à ces pages pour les citations. Le pont n’effectue aucune signature et ne formule aucune affirmation de conformité de signature.
Voir aussi
Section intitulée « Voir aussi »- Effectuer le rendu HTML vers PDF avec le renderer Chrome Artisan — le renderer exécuté dans le processus utilisé comme repli local ici.
- Démarrage rapide Cloudflare — le premier rendu en périphérie et le modèle de résultat.
- Sécurité et opérations Cloudflare — SSRF, DNS rebinding, épinglage et rotation des secrets.
- Usage en production Cloudflare — câblage du repli, télémétrie, archivage R2 et protection de l’API.