Aller au contenu

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.

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.

À lire directement dans CloudflareHtmlRenderer :

SituationRésultat
Configuration incomplète, fallbackToLocal: falseCloudflareNotAvailableException
Configuration incomplète, fallbackToLocal: true, fabrique câbléeRendu local
Le Worker lève une erreur de transport, repli activé, fabrique câbléeRendu local, journalisé en warning puis en info
Le Worker lève, repli activé, Artisan installé, aucune fabriqueCloudflareNotAvailableException 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.

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.

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 exemple invoices/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 devient document.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éfaut 104857600) est rejeté avant toute requête, ce qui renvoie un R2UploadResult avec success: false.
  • R2UploadResult::isValid() exige success, ainsi qu’une key non vide et un etag non vide.
$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.

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.

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 denialReason spécifique.
  • ApiKeyValidator::validate() utilise hash_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éfaut 60) et une fenêtre par heure (fixée à 3600 secondes). 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 toujours X-Content-Type-Options: nosniff et X-Frame-Options: DENY, et fusionne les en-têtes de limite de débit (X-RateLimit-Remaining, X-RateLimit-Reset, plus Retry-After en cas de refus).

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 :

  1. render()CloudflareRenderResult::$pdfData.
  2. 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.

  • /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.