Rendering am Edge mit Cloudflare und lokalem Fallback
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Die Cloudflare-Brücke sendet Ihr HTML an einen Cloudflare-Worker-Render-Endpunkt und gibt das PDF zurück. Das Rendering läuft am Edge, ohne dass Sie einen langlebigen Browser-Prozess betreiben müssen. Sie erstellen eine reine HTTPS-Konfiguration, binden einen PSR-18-Client und PSR-17-Factories ein, rufen render() auf und richten optional einen lokalen Renderer ein, auf den die Brücke zurückfällt, wenn der Worker nicht erreichbar ist. Dieser Leitfaden behandelt den Render-Aufruf, die Fallback-Entscheidung und die SSRF-, DNS-Rebinding- und TLS-Public-Key-Pinning-Kontrollen, die die Brücke erzwingt, bevor eine Anfrage den Prozess verlässt.
Vorab die Voraussetzungen:
- NextPDF core und
nextpdf/cloudflaresind installiert. - Ein Worker-Endpunkt erfüllt den Render-Kontrakt über HTTPS und akzeptiert ein Bearer-Token. Die Brücke weist eine Worker-URL ohne HTTPS ab, bevor sie etwas sendet.
- Ein PSR-18-Client (zum Beispiel Guzzle 7) sowie PSR-17-Request- und -Stream-Factories sind verfügbar. Für den gepinnten cURL-Transport stellen Sie zusätzlich eine PSR-17-Response-Factory und
ext-curlbereit. - Für den lokalen Fallback steht
nextpdf/artisan(oder ein anderer lokaler Renderer) bereit.
Dies ist eine Anleitung. Für ein erstes lauffähiges Rendering lesen Sie den Cloudflare-Quickstart.
Installation
Abschnitt betitelt „Installation“Installieren Sie die Brücke, einen PSR-18-Client und PSR-17-Factories.
composer require nextpdf/cloudflare guzzlehttp/guzzleInstallieren Sie für den lokalen Fallback einen lokalen Renderer, an den die Brücke delegieren kann.
composer require nextpdf/artisanBeziehen Sie das Worker-Bearer-Token und etwaige R2-Anmeldedaten aus Umgebungsvariablen oder einem Secrets-Manager. Übernehmen Sie sie niemals in einen Commit.
Konzeptueller Überblick
Abschnitt betitelt „Konzeptueller Überblick“CloudflareHtmlRenderer::render() validiert das HTML und das Ziel, sendet einen authentifizierten POST an den Worker und parst die Antwort. Der Worker gibt entweder rohe PDF-Bytes zurück (Content-Type: application/pdf) oder einen JSON-Body mit einem base64-codierten pdf-Feld. Der Renderer bildet das Ergebnis auf ein final readonly CloudflareRenderResult ab, das die Bytes, die angeforderte Breite, die Höhe, den aus dem CF-Ray-Header abgeleiteten Render-Ort und die Render-Zeit enthält.
Die Brücke unterscheidet bewusst zwischen zwei Fehlerklassen:
CloudflareRenderException— der Worker hat geantwortet, aber das Rendern ist fehlgeschlagen (ein HTTP-Fehler oder ein Body, der nicht mit%PDFbeginnt). Das ist ein Render-Fehler und wird niemals mit einem Fallback wiederholt.CloudflareNotAvailableException— der Edge war nicht erreichbar und es stand kein nutzbarer Fallback bereit.
Der lokale Fallback deckt den zweiten Fall ab. Wenn der Worker nicht erreichbar ist und fallbackToLocal auf true steht, ruft die Brücke ein von Ihnen bereitgestelltes LocalRendererFactoryInterface auf, und zwar lazy — das create() der Factory läuft nur auf dem Fallback-Pfad. Bei einem Fallback-Render ist die renderLocation des Ergebnisses der literale String local.
Die Brücke schützt die Netzwerkgrenze, bevor eine Anfrage PHP verlässt. Sie weist eine Worker-URL ohne HTTPS ab. Sie weist einen Worker-Host ab, der in privaten oder reservierten Adressraum auflöst, und prüft dabei alle A- und AAAA-Records statt nur den ersten. Außerdem löst sie den Host unmittelbar vor dem Verbinden erneut auf, was das time-of-check/time-of-use-Fenster gegen DNS-Rebinding schließt. Wenn Sie eine PSR-17-Response-Factory und entweder ein aufgelöstes IP-Set oder SPKI-Pins bereitstellen, nutzt die Brücke einen gepinnten cURL-Transport. Dieser Transport bindet die Verbindung an die geprüften IPs (CURLOPT_RESOLVE), erzwingt TLS-Public-Key-Pinning (CURLOPT_PINNEDPUBLICKEY), verifiziert Peer und Host und folgt keinen Redirects.
API-Oberfläche
Abschnitt betitelt „API-Oberfläche“// 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() verwendet standardmäßig die A4-Breite (595.28 Punkt) und eine automatisch ermittelte Höhe (heightPt: 0). Die vollständige Feldreferenz und die fromArray()-Schlüsselzuordnung findest du auf der unter „Siehe auch“ verlinkten Cloudflare-Konfigurationsseite.
Codebeispiel — Schnellstart
Abschnitt betitelt „Codebeispiel — Schnellstart“Konstruieren Sie die Config, bauen Sie den Renderer, rendern Sie und schreiben Sie die Bytes.
<?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);}Das Token wird aus der Umgebung gelesen und niemals fest im Code hinterlegt. workerUrl muss HTTPS sein; die Brücke weist eine http://-URL ab, bevor sie eine Anfrage sendet.
Codebeispiel — Produktion
Abschnitt betitelt „Codebeispiel — Produktion“Binden Sie in der Produktion eine lokale Renderer-Factory ein, damit bei einem nicht erreichbaren Worker auf den lokalen Renderer zurückgefallen wird, statt die Anfrage scheitern zu lassen, und konfigurieren Sie TLS-Pins mit einem Backup-Pin. Das create() der Factory läuft nur auf dem Fallback-Pfad.
<?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(); } }; }}Binden Sie die Factory und die Pins in den Renderer ein.
<?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,);Wenn der Fallback läuft, ist die renderLocation des Ergebnisses local und heightPt ist 0.0. Die Brücke protokolliert den Fallback zuerst auf warning, dann auf info. Konfigurieren Sie vor einer Zertifikatsrotation stets einen Backup-Pin, damit eine geplante Rotation die Brücke nicht vom Endpunkt aussperrt.
Sonderfälle & Fallstricke
Abschnitt betitelt „Sonderfälle & Fallstricke“- Ein Worker-Fehler ist kein Erreichbarkeitsfehler. Ein Worker, der mit einem HTTP-Fehler oder einem fehlerhaften Body antwortet, löst
CloudflareRenderExceptionaus und wird niemals mit dem Fallback wiederholt. Nur bei einem nicht erreichbaren Edge wird zurückgefallen. Halten Sie die beiden catch-Zweige getrennt. - Der Fallback braucht sowohl das Flag als auch eine Factory. Bei
fallbackToLocal: true, aber ohne eingebundene Factory löst ein nicht erreichbarer Worker eineCloudflareNotAvailableExceptionaus, die die fehlende Factory benennt. Binden Sie die Factory ein. isAvailable()ist ein Hinweis, keine Garantie. Sie sendet einen authentifiziertenHEADund gibttruefür einen Status unter500zurück; der folgendePOSTkann dennoch fehlschlagen. Behandeln Sie dies nicht als Kontrakt.- Pinning ist opt-in. Ein leeres Pin-Set deaktiviert das Pinning. Verwenden Sie ein leeres Set nur mit einer stabilen, bekannten Zertifikatskette und halten Sie einen Backup-Pin vor, sobald Sie pinnen.
fontFilesbraucht einen R2-Bucket. DasfontFiles-Argument ist nur relevant, wenn die Configr2FontBucketsetzt; andernfalls hat es keine Wirkung.- Die Brücke signiert nicht. Sie gibt PDF-Bytes zurück. Rendern Sie am Edge und signieren Sie anschließend im eigenen Prozess, damit der Signaturschlüssel niemals die Edge-Grenze überschreitet.
Performance
Abschnitt betitelt „Performance“Edge-Rendering verlagert die Browser-Kosten vollständig von Ihren Hosts weg. Was stattdessen anfällt, ist ein HTTPS-Round-Trip zum Worker plus die eigentliche Render-Zeit des Workers, die das Ergebnis als renderTimeMs meldet. Die Brücke wendet das konfigurierte Timeout über den gepinnten Transport an. Setzen Sie es anhand der gemessenen Worker-Latenz mit Puffer und halten Sie es unter jedem vorgelagerten Gateway-Timeout. Das Paket nennt nur die Limits, die es selbst erzwingt. Es trifft keine Aussage über CPU-, Speicher- oder Request-Body-Obergrenzen der Cloudflare-Plattform. Konsultieren Sie dafür die Dokumentation von Cloudflare und Ihren Worker.
Sicherheitshinweise
Abschnitt betitelt „Sicherheitshinweise“- Das Ziel wird validiert, bevor die Anfrage PHP verlässt. URLs ohne HTTPS werden abgewiesen. Ein Host, der in privaten oder reservierten Adressraum auflöst, wird über alle A- und AAAA-Records hinweg abgewiesen. Der Host wird unmittelbar vor dem Verbinden erneut aufgelöst, um vor DNS-Rebinding zu schützen.
- Der gepinnte Transport bindet DNS und TLS. Mit konfigurierter Response-Factory und Pins bindet die Brücke die Verbindung an die geprüften IPs, erzwingt SPKI-Pinning, verifiziert Peer und Host und folgt keinen Redirects zu einem ungeprüften Host.
- Die Eingabe ist begrenzt. HTML über
maxHtmlSize(Standard 5 MB), ein übergroßer base64-Data-URI und jedes<meta http-equiv="refresh">-Tag werden abgewiesen, bevor die Anfrage gesendet wird. - Secrets werden geschwärzt und sind unveränderlich.
apiTokenund R2-Schlüssel tragen#[SensitiveParameter], sodass sie aus Stack-Traces geschwärzt werden, und die Config-Objekte sindfinal readonly. Beziehen Sie Secrets aus der Umgebung oder einem Secrets-Manager; übernehmen Sie sie niemals in einen Commit. - Schreiben Sie niemals einen leeren
catch-Block. Jedes Beispiel fängt den spezifischen Exception-Typ ab und protokolliert oder beendet mit einem definierten Code.
Das vollständige Sicherheitsmodell — die SSRF- und DNS-Rebinding-Abwehr, die operative Pinning-Anleitung und der Umgang mit Secrets — finden Sie auf der unter „Siehe auch“ verlinkten Cloudflare-Seite zu Sicherheit und Betrieb, die die relevanten OWASP- und RFC 7469-Klauseln verankert.
Konformität
Abschnitt betitelt „Konformität“Dieser Leitfaden erhebt keinen eigenen normativen Standardsanspruch. Die vorgelagerten Cloudflare-Seiten zu Sicherheit und Betrieb sowie zur Konfiguration ordnen die DNS-Auflösung über alle Records und die TOCTOU-Neuprüfung der Brücke der OWASP-SSRF-Präventionsanleitung zu; ihr TLS-Public-Key-Pinning und die Backup-Pin-Wiederherstellung ordnen sie RFC 7469 zu. Diese Cookbook-Seite beschreibt die Verwendung und überlässt jene Zitate diesen Seiten. Die Brücke führt keine Signierung durch und erhebt keinen Signaturkonformitätsanspruch.
Siehe auch
Abschnitt betitelt „Siehe auch“- HTML mit dem Artisan-Chrome-Renderer zu PDF rendern — der In-Process-Renderer, der hier als lokaler Fallback dient.
- Cloudflare-Quickstart — das erste Edge-Rendering und das Ergebnismodell.
- Cloudflare-Sicherheit und -Betrieb — SSRF, DNS-Rebinding, Pinning und Secret-Rotation.
- Cloudflare-Produktionseinsatz — Fallback-Verdrahtung, Telemetrie, R2-Archivierung und API-Schutz.