Usage en production — repli, télémétrie, archivage, protection
Cette page couvre quatre aspects de production que le paquet prend en charge en plus du rendu brut : le repli local, la télémétrie de la périphérie, l’archivage R2 et la couche de protection d’API entrante. Chaque section s’appuie sur un comportement vérifié dans les classes.
Repli local
Section intitulée « Repli local »Lorsque le Worker est injoignable et que fallbackToLocal vaut true, le pont délègue à un moteur de rendu local. Fournis ce moteur de rendu local via LocalRendererFactoryInterface. Le pont n’appelle la fabrique qu’à la demande, si bien que le create() de la fabrique ne s’exécute que sur le chemin de repli.
<?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); } }; }}Câble la fabrique sur le moteur de rendu :
use NextPDF\Cloudflare\CloudflareHtmlRenderer;
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory, logger: $logger, localRendererFactory: new ArtisanLocalRendererFactory($chrome), responseFactory: $httpFactory,);Lorsque le repli est utilisé, le renderLocation du résultat est la chaîne littérale local et heightPt vaut 0.0. Le chemin local ne remonte ni emplacement de périphérie ni hauteur mesurée. Le pont transmet la largeur demandée au moteur de rendu local via la clé d’option widthPt.
Logique de décision du repli
Section intitulée « Logique de décision du repli »À lire directement dans CloudflareHtmlRenderer :
| Situation | Résultat |
|---|---|
Configuration incomplète, fallbackToLocal: false | CloudflareNotAvailableException |
Configuration incomplète, fallbackToLocal: true, fabrique câblée | Rendu local |
| Le Worker lève une erreur de transport, repli activé, fabrique câblée | Rendu local, journalisé en warning puis en info |
| Le Worker lève, repli activé, Artisan installé, aucune fabrique | CloudflareNotAvailableException qui nomme la fabrique manquante |
| Le Worker lève, repli activé, Artisan non installé | CloudflareNotAvailableException qui nomme le paquet manquant |
| Le Worker renvoie une erreur HTTP / un corps mal formé | CloudflareRenderException, ne se replie jamais |
La dernière ligne porte la distinction cruciale. Un Worker qui répond par une erreur correspond à un échec de rendu, pas à un problème d’accessibilité. L’erreur est relancée pour que ton code puisse distinguer un rendu cassé d’une périphérie injoignable.
Télémétrie de la périphérie
Section intitulée « Télémétrie de la périphérie »Chaque rendu réussi sur le chemin binaire contient une télémétrie dérivée des en-têtes de réponse :
$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(),]);Le moteur de rendu lit renderLocation depuis l’en-tête de réponse CF-Ray, en prenant le segment situé après le dernier trait d’union. Pour CF-Ray: 8abc123def456-TPE, l’emplacement est TPE. Lorsque l’en-tête est absent, l’emplacement est une chaîne vide. Sur le chemin de réponse JSON, la valeur provient plutôt du champ JSON renderLocation. Traite ces valeurs comme des signaux d’observabilité émis par le Worker, et non comme des garanties de la plateforme.
Archivage R2
Section intitulée « Archivage R2 »R2ArchiveManager téléverse les octets PDF vers Cloudflare R2 via l’API compatible S3, avec des requêtes signées au moyen d’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]);}Comportement vérifié dans R2ArchiveManager et R2ObjectKey :
- La clé d’objet est partitionnée par date :
<pathPrefix><Y>/<m>/<d>/<sanitized-filename>, par exempleinvoices/2026/05/18/invoice-2026-0042.pdf. - Le nom de fichier est assaini :
basename()est appliqué (suppression de la traversée de répertoire), puis les octets nuls et les caractères de contrôle (\x00–\x1f,\x7f) sont retirés. Si le résultat est vide, il devientdocument.pdf. - Les métadonnées personnalisées sont envoyées sous forme d’en-têtes
x-amz-meta-<lowercased-key>, incluses dans l’ensemble des en-têtes signés V4. - Un téléversement supérieur à
maxFileSizeBytes(par défaut104857600) est rejeté avant toute requête, ce qui renvoie unR2UploadResultavecsuccess: false. R2UploadResult::isValid()exigesuccess, ainsi qu’unekeynon vide et unetagnon vide.
URL de téléchargement présignées
Section intitulée « URL de téléchargement présignées »$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);generateSignedUrl() construit une URL GET signée par requête avec AWS Signature V4, avec un X-Amz-Expires que tu contrôles (par défaut 3600 secondes). La requête canonique utilise la sentinelle de hachage de contenu UNSIGNED-PAYLOAD. Une URL de lecture signée par requête utilise cette forme, car le corps ne fait pas partie de la requête signée. Cela décrit le comportement de signature implémenté par le paquet, tel qu’il est lu dans R2ArchiveManager. La documentation du service Amazon définit AWS Signature Version 4, et non une norme d’OND, si bien qu’aucune clause normative n’est épinglée ici. Les clés d’accès aux objets sont #[SensitiveParameter] ; garde-les hors des journaux.
URL publiques
Section intitulée « URL publiques »R2UploadResult::publicUrl($customDomain) renvoie la clé brute lorsqu’aucun domaine n’est fourni, ou https://<domain>/<key> sinon. Elle impose le schéma HTTPS lorsque le domaine fourni n’en a aucun. Elle ne rend pas public un bucket privé. Cela dépend de la configuration du bucket R2.
Protection de l’API entrante
Section intitulée « Protection de l’API entrante »ApiProtection est la couche que tu appliques aux requêtes de rendu qui arrivent sur une passerelle PHP placée devant le Worker. Elle exécute trois vérifications dans un ordre fixe : clé d’API, puis taille de la charge utile, puis limite de débit.
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;}Comportement vérifié :
- L’ordre est : clé d’API → taille de la charge utile → limite de débit. La première vérification qui échoue court-circuite avec un
denialReasonspécifique. ApiKeyValidator::validate()utilisehash_equals()pour une comparaison à temps constant et rejette une clé vide.validateHashed()compare aux empreintes SHA-256 pour le stockage des clés au repos. Les paramètres de clé portent#[SensitiveParameter].- Le magasin de limite de débit est en mémoire, par processus. Il suit une fenêtre par minute (
rateLimitWindowSeconds, par défaut60) et une fenêtre par heure (fixée à3600secondes). Il ne persiste pas d’un Worker à l’autre ni après un redémarrage. Pour une limite partagée entre processus, adosse-le à un magasin partagé. ApiProtectionResult::toHeaders()ajoute toujoursX-Content-Type-Options: nosniffetX-Frame-Options: DENY, et fusionne les en-têtes de limite de débit (X-RateLimit-Remaining,X-RateLimit-Reset, plusRetry-Afteren cas de refus).
Rendu puis signature
Section intitulée « Rendu puis signature »Ce pont ne signe pas les PDF. Un pipeline de signature en production effectue le rendu à la périphérie, puis signe les octets renvoyés avec le moteur :
render()→CloudflareRenderResult::$pdfData.- Confie
$pdfDataànextpdf/core(ou à NextPDF Pro pour la signature PAdES B-B). Les profils de validation à long terme sont une fonctionnalité Enterprise ; ce pont core ne revendique ni l’une ni l’autre.
Garde l’étape de signature dans ton propre processus afin que la clé de signature ne franchisse jamais la frontière de la périphérie.
Voir aussi
Section intitulée « Voir aussi »- /integrations/cloudflare/security-and-operations/ — épinglage, défense SSRF, rotation des secrets, runbook d’exploitation.
- /integrations/cloudflare/troubleshooting/ — catalogue des modes de défaillance.
- /integrations/cloudflare/configuration/ — chaque champ et valeur par défaut.