Aan de edge renderen met Cloudflare en lokale fallback
In één oogopslag
Sectie met titel “In één oogopslag”De Cloudflare-bridge stuurt uw HTML naar een Cloudflare Worker-render-endpoint en retourneert de PDF. Het renderen gebeurt aan de edge, zodat u geen langlopend browserproces hoeft te beheren. U maakt een config die alleen HTTPS toestaat, koppelt een PHP Standards Recommendation (PSR)-18-client en PSR-17-factory’s, roept render() aan en kunt een lokale renderer toevoegen voor Workers die niet bereikbaar zijn. Deze gids toont de render-aanroep, het fallbackpad en de maatregelen voor Server-Side Request Forgery (SSRF), Domain Name System (DNS)-rebinding en Transport Layer Security (TLS) public-key-pinning die de bridge afdwingt voordat een verzoek het proces verlaat.
Vereisten, vooraf:
- NextPDF core en
nextpdf/cloudflarezijn geïnstalleerd. - Een Worker-endpoint biedt het render-contract via HTTPS aan en accepteert een bearertoken. De bridge weigert een Worker-URL zonder HTTPS voordat er iets wordt verzonden.
- Een PSR-18-client (bijvoorbeeld Guzzle 7) en PSR-17-request- en stream-factory’s zijn beschikbaar. Voor het gepinde cURL-transport levert u ook een PSR-17-response-factory en
ext-curl. - Voor lokale fallback is
nextpdf/artisan(of een andere lokale renderer) beschikbaar.
Dit is een how-to. Begin voor uw eerste uitvoerbare render met de Cloudflare-quickstart.
Installeren
Sectie met titel “Installeren”Installeer de bridge, een PSR-18-client en PSR-17-factory’s.
composer require nextpdf/cloudflare guzzlehttp/guzzleInstalleer voor lokale fallback een lokale renderer die de bridge kan aanroepen.
composer require nextpdf/artisanLaad het Worker-bearertoken en eventuele R2-referenties uit omgevingsvariabelen of een secrets manager. Commit ze nooit.
Conceptueel overzicht
Sectie met titel “Conceptueel overzicht”CloudflareHtmlRenderer::render() valideert de HTML en de bestemming, stuurt een geauthenticeerde POST naar de Worker en parseert het antwoord. De Worker retourneert ruwe PDF-bytes (Content-Type: application/pdf) of een JSON-body met een base64-veld pdf. De renderer zet het antwoord om naar een final readonly CloudflareRenderResult met de bytes, de gevraagde breedte, de hoogte, de render-locatie (afgeleid van de CF-Ray-header) en de render-tijd.
De bridge deelt fouten op in twee expliciete klassen:
CloudflareRenderException— de Worker heeft geantwoord, maar de render is mislukt (een HTTP-fout of een body die niet begint met%PDF). Dit is een renderfout en wordt nooit opnieuw geprobeerd met een fallback.CloudflareNotAvailableException— de edge kon niet worden bereikt en er was geen bruikbare fallback beschikbaar.
Lokale fallback dekt het tweede geval. Wanneer de Worker niet bereikbaar is en fallbackToLocal true is, roept de bridge de LocalRendererFactoryInterface aan die u opgeeft. Dit gebeurt lazy: de create() van de factory draait alleen op het fallbackpad. Bij een fallback-render is de renderLocation van het resultaat de letterlijke tekenreeks local.
De bridge beschermt de netwerkgrens voordat een verzoek PHP verlaat. De bridge weigert een Worker-URL zonder HTTPS. Ook weigert de bridge een Worker-host die naar private of gereserveerde adresruimte resolvet, en controleert daarbij alle A- en AAAA-records in plaats van alleen het eerste. De bridge resolvet de host opnieuw vlak voordat er verbinding wordt gemaakt, waardoor het time-of-check/time-of-use (TOCTOU)-venster tegen DNS-rebinding wordt gesloten. Wanneer u een PSR-17-response-factory levert plus ofwel een set geresolvede IP’s ofwel Subject Public Key Info (SPKI)-pins, gebruikt de bridge een gepind cURL-transport. Dat transport bindt de verbinding aan de geverifieerde IP’s (CURLOPT_RESOLVE), dwingt TLS public-key-pinning af (CURLOPT_PINNEDPUBLICKEY), verifieert de peer en host en volgt geen redirects.
API-oppervlak
Sectie met titel “API-oppervlak”// 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() gebruikt standaard de A4-breedte (595.28 punten) en een automatisch gedetecteerde hoogte (heightPt: 0). Raadpleeg voor de volledige veldreferentie en de sleutelmapping van fromArray() de Cloudflare-configuratiepagina onder Zie ook.
Codevoorbeeld — Quickstart
Sectie met titel “Codevoorbeeld — Quickstart”Maak de config, bouw de renderer, render en schrijf de bytes weg.
<?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);}Het token komt uit de omgeving en wordt nooit hardcoded. workerUrl moet HTTPS gebruiken; de bridge weigert een http://-URL voordat er een verzoek wordt verzonden.
Codevoorbeeld — Productie
Sectie met titel “Codevoorbeeld — Productie”Koppel in productie een lokale renderer-factory, zodat een onbereikbare Worker kan terugvallen in plaats van het verzoek te laten mislukken. Configureer TLS-pins met een backuppin. De create() van de factory draait alleen op het fallbackpad.
<?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(); } }; }}Koppel de factory en pins in de 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,);Wanneer de fallback draait, is de renderLocation van het resultaat local en is heightPt 0.0. De bridge logt de fallback eerst op warning en daarna op info. Configureer altijd een backuppin voordat u het certificaat roteert, zodat een geplande rotatie de bridge niet buitensluit van het endpoint.
Randgevallen en valkuilen
Sectie met titel “Randgevallen en valkuilen”- Een Worker-fout is geen bereikbaarheidsfout. Een Worker die een HTTP-fout of een misvormde body retourneert, gooit
CloudflareRenderExceptionen wordt nooit opnieuw geprobeerd met de fallback. Alleen een onbereikbare edge valt terug. Houd de twee catch-takken gescheiden. - Fallback heeft zowel de flag als een factory nodig. Met
fallbackToLocal: truemaar zonder gekoppelde factory gooit een onbereikbare WorkerCloudflareNotAvailableExceptionen noemt de ontbrekende factory. Koppel de factory. isAvailable()is een aanwijzing, geen garantie. Het stuurt een geauthenticeerdeHEADen retourneerttruevoor een status onder500; de daaropvolgendePOSTkan alsnog mislukken. Behandel dit niet als een contract.- Pinning is opt-in. Een lege pinset schakelt pinning uit. Gebruik een lege set alleen bij een stabiele, bekende certificaatketen, en houd een backuppin aan zodra u pint.
fontFilesvereist een R2-bucket. Het argumentfontFilesdoet alleen ter zake wanneer de configr2FontBucketinstelt; anders heeft het geen effect.- De bridge ondertekent niet. De bridge retourneert PDF-bytes. Render aan de edge en onderteken vervolgens in uw eigen proces, zodat de ondertekeningssleutel de edge-grens nooit passeert.
Prestaties
Sectie met titel “Prestaties”Edge-rendering verplaatst de browserkosten weg van uw hosts. U betaalt nog steeds voor één HTTPS-round-trip naar de Worker plus de render-tijd van de Worker, die het resultaat rapporteert als renderTimeMs. De bridge past de geconfigureerde time-out toe via het gepinde transport. Stel deze in op basis van gemeten Worker-latentie met enige marge, en houd deze onder elke upstream-gateway-time-out. Het pakket vermeldt alleen de limieten die het zelf afdwingt. Het doet geen uitspraak over de CPU-, geheugen- of request-body-plafonds van het Cloudflare-platform. Raadpleeg voor die limieten de documentatie van Cloudflare en uw Worker.
Beveiligingsnotities
Sectie met titel “Beveiligingsnotities”- De bestemming wordt gevalideerd voordat het verzoek PHP verlaat. URL’s zonder HTTPS worden geweigerd. Een host die naar private of gereserveerde adresruimte resolvet, wordt geweigerd voor alle A- en AAAA-records. De host wordt opnieuw geresolved vlak voor het verbinden, ter verdediging tegen DNS-rebinding.
- Het gepinde transport bindt DNS en TLS. Met een response-factory en geconfigureerde pins bindt de bridge de verbinding aan de geverifieerde IP’s, dwingt SPKI-pinning af, verifieert de peer en host en weigert redirects naar een niet-geverifieerde host te volgen.
- Invoer is begrensd. HTML boven
maxHtmlSize(standaard 5 MB), een te grote base64-data-URI en elke<meta http-equiv="refresh">-tag worden geweigerd voordat het verzoek wordt verzonden. - Secrets worden geredigeerd en zijn immutable.
apiTokenen R2-sleutels dragen#[SensitiveParameter], zodat stacktraces deze redigeren, en de config-objecten zijnfinal readonly. Laad secrets uit de omgeving of een secrets manager; commit ze nooit. - Schrijf nooit een leeg
catch-blok. Elk voorbeeld vangt het specifieke uitzonderingstype af en logt of stopt met een gedefinieerde code.
Het volledige beveiligingsmodel staat op de Cloudflare-pagina over beveiliging en operations onder Zie ook. Die pagina behandelt de verdediging tegen SSRF en DNS-rebinding, pinning-operations, het omgaan met secrets en de relevante OWASP- en RFC 7469-clausules.
Conformiteit
Sectie met titel “Conformiteit”Deze gids doet geen eigen normatieve standaardenclaim. Op de upstream Cloudflare-pagina’s over beveiliging en operations en over configuratie sluiten de all-records-DNS-resolutie en TOCTOU-hercontrole van de bridge aan op de OWASP-richtlijnen voor SSRF-preventie, en sluiten TLS-public-key-pinning en het backuppin-herstel aan op RFC 7469. Deze cookbook-pagina herhaalt het gebruik en laat die citaten over aan die pagina’s. De bridge voert geen ondertekening uit en doet geen claim over handtekeningconformiteit.
Zie ook
Sectie met titel “Zie ook”- HTML naar PDF renderen met de Artisan Chrome-renderer — de in-process renderer die hier als lokale fallback wordt gebruikt.
- Cloudflare-quickstart — de eerste edge-render en het resultaatmodel.
- Cloudflare-beveiliging en -operations — SSRF, DNS-rebinding, pinning en secret-rotatie.
- Cloudflare productiegebruik — fallback-koppeling, telemetrie, R2-archivering en API-bescherming.