Render di edge dengan Cloudflare dan fallback lokal
Sekilas pandang
Bagian berjudul “Sekilas pandang”Bridge Cloudflare mengirim HTML Anda ke endpoint render Cloudflare Worker dan mengembalikan PDF. Proses render berjalan di edge, sehingga Anda tidak perlu menjalankan proses browser berumur panjang. Anda membuat konfigurasi yang hanya menerima HTTPS, menyambungkan klien PHP Standards Recommendation (PSR)-18 dan factory PSR-17, memanggil render(), dan dapat menambahkan renderer lokal ketika Worker tidak dapat dijangkau. Panduan ini memperlihatkan pemanggilan render, jalur fallback, serta kontrol Server-Side Request Forgery (SSRF), DNS-rebinding (Domain Name System), dan public-key-pinning Transport Layer Security (TLS) yang diberlakukan bridge sebelum permintaan apa pun keluar dari proses.
Prasyarat awal:
- Core NextPDF dan
nextpdf/cloudflaretelah terpasang. - Sebuah endpoint Worker melayani kontrak render melalui HTTPS dan menerima bearer token. Bridge menolak URL Worker non-HTTPS sebelum mengirim apa pun.
- Klien PSR-18 (misalnya Guzzle 7) serta factory request dan stream PSR-17 tersedia. Untuk transport cURL yang di-pin, sediakan juga response factory PSR-17 dan
ext-curl. - Untuk fallback lokal,
nextpdf/artisan(atau renderer lokal lain) tersedia.
Ini adalah panduan praktis. Untuk render pertama yang dapat dijalankan, mulailah dengan quickstart Cloudflare.
Instalasi
Bagian berjudul “Instalasi”Pasang bridge, klien PSR-18, dan factory PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzleUntuk fallback lokal, pasang renderer lokal yang dapat dipanggil oleh bridge.
composer require nextpdf/artisanMuat bearer token Worker dan kredensial R2 yang diperlukan dari variabel lingkungan atau pengelola rahasia. Jangan pernah meng-commit nilai tersebut.
Gambaran konseptual
Bagian berjudul “Gambaran konseptual”CloudflareHtmlRenderer::render() memvalidasi HTML dan tujuan, mengirim POST terautentikasi ke Worker, lalu mem-parsing respons. Worker mengembalikan byte PDF mentah (Content-Type: application/pdf) atau body JSON dengan field pdf base64. Renderer memetakan respons menjadi final readonly CloudflareRenderResult yang membawa byte, lebar yang diminta, tinggi, lokasi render (diturunkan dari header CF-Ray), dan waktu render.
Bridge membagi kegagalan ke dalam dua kelas eksplisit:
CloudflareRenderException— Worker menjawab tetapi proses render gagal (galat HTTP atau body yang tidak diawali dengan%PDF). Ini adalah kegagalan render dan tidak pernah dicoba ulang dengan fallback.CloudflareNotAvailableException— edge tidak dapat dijangkau dan tidak ada fallback yang dapat digunakan.
Fallback lokal menangani kasus kedua. Ketika Worker tidak dapat dijangkau dan fallbackToLocal bernilai true, bridge memanggil LocalRendererFactoryInterface yang Anda sediakan. Proses ini dilakukan secara lazy: create() milik factory hanya berjalan pada jalur fallback. Pada render fallback, renderLocation pada hasil adalah string literal local.
Bridge melindungi batas jaringan sebelum permintaan apa pun keluar dari PHP. Bridge menolak URL Worker non-HTTPS. Bridge menolak host Worker yang hasil resolve-nya berada di ruang alamat privat atau dicadangkan, dengan memeriksa semua record A dan AAAA, bukan hanya record pertama. Bridge juga melakukan resolve ulang host tepat sebelum menyambung, sehingga menutup jendela time-of-check/time-of-use (TOCTOU) terhadap DNS rebinding. Ketika Anda menyediakan response factory PSR-17 dan salah satu dari kumpulan IP hasil resolve atau pin Subject Public Key Info (SPKI), bridge menggunakan transport cURL yang di-pin. Transport tersebut mengikat koneksi ke IP yang telah diverifikasi (CURLOPT_RESOLVE), memberlakukan public-key pinning TLS (CURLOPT_PINNEDPUBLICKEY), memverifikasi peer dan host, serta tidak mengikuti redirect.
Permukaan API
Bagian berjudul “Permukaan 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() secara default menggunakan lebar A4 (595.28 poin) dan tinggi yang dideteksi otomatis (heightPt: 0). Untuk referensi lengkap tentang field dan peta kunci fromArray(), lihat halaman konfigurasi Cloudflare pada Lihat juga.
Contoh kode — Mulai cepat
Bagian berjudul “Contoh kode — Mulai cepat”Buat konfigurasi, buat renderer, render, lalu tulis byte-nya.
<?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);}Token diambil dari lingkungan dan tidak pernah di-hard-code. workerUrl harus menggunakan HTTPS; bridge menolak URL http:// sebelum mengirim permintaan apa pun.
Contoh kode — Produksi
Bagian berjudul “Contoh kode — Produksi”Di produksi, sambungkan factory renderer lokal agar permintaan melakukan fallback ketika Worker tidak dapat dijangkau, bukan langsung gagal. Konfigurasikan pin TLS dengan pin cadangan. create() milik factory hanya berjalan pada jalur fallback.
<?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(); } }; }}Hubungkan factory dan pin ke 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,);Ketika fallback berjalan, renderLocation pada hasil bernilai local dan heightPt bernilai 0.0. Bridge mencatat fallback pada level warning, lalu info. Selalu konfigurasikan pin cadangan sebelum rotasi sertifikat, agar rotasi terencana tidak membuat bridge terkunci dari endpoint.
Kasus tepi & jebakan
Bagian berjudul “Kasus tepi & jebakan”- Galat Worker bukan kegagalan keterjangkauan. Worker yang mengembalikan galat HTTP atau body malformed memunculkan
CloudflareRenderExceptiondan tidak pernah dicoba ulang dengan fallback. Fallback hanya dipakai ketika edge tidak dapat dijangkau. Pertahankan kedua cabang catch tetap terpisah. - Fallback membutuhkan flag sekaligus factory. Dengan
fallbackToLocal: truetetapi tanpa factory yang tersambung, kondisi Worker yang tidak dapat dijangkau memunculkanCloudflareNotAvailableExceptiondan menyebutkan factory yang hilang. Sambungkan factory. isAvailable()adalah petunjuk, bukan jaminan. Metode ini mengirimHEADterautentikasi dan mengembalikantrueuntuk status di bawah500;POSTberikutnya tetap dapat gagal. Jangan memperlakukannya sebagai kontrak.- Pinning bersifat opt-in. Kumpulan pin kosong menonaktifkan pinning. Gunakan kumpulan kosong hanya dengan rantai sertifikat yang stabil dan diketahui, dan sediakan pin cadangan begitu Anda melakukan pinning.
fontFilesmembutuhkan bucket R2. ArgumenfontFileshanya berpengaruh ketika konfigurasi menetapkanr2FontBucket; jika tidak, argumen ini tidak memiliki efek.- Bridge tidak menandatangani. Ia mengembalikan byte PDF. Render dilakukan di edge, lalu tandatangani dalam proses Anda sendiri, sehingga kunci penanda tangan tidak pernah melintasi batas edge.
Performa
Bagian berjudul “Performa”Rendering di edge memindahkan beban browser dari host Anda. Anda tetap menanggung satu round trip HTTPS ke Worker ditambah waktu render Worker, yang dilaporkan oleh hasil sebagai renderTimeMs. Bridge menerapkan timeout yang dikonfigurasi melalui transport yang di-pin. Tetapkan nilainya berdasarkan latensi Worker yang terukur dengan margin, dan jaga agar tetap di bawah timeout gateway upstream mana pun. Paket ini hanya menyatakan batas yang diberlakukannya sendiri. Paket ini tidak membuat klaim apa pun tentang batas atas CPU, memori, atau body permintaan pada platform Cloudflare. Untuk batas tersebut, rujuk dokumentasi Cloudflare dan Worker Anda.
Catatan keamanan
Bagian berjudul “Catatan keamanan”- Tujuan divalidasi sebelum permintaan keluar dari PHP. URL non-HTTPS ditolak. Host yang hasil resolve-nya berada di ruang alamat privat atau dicadangkan ditolak di seluruh record A dan AAAA. Host di-resolve ulang tepat sebelum menyambung untuk bertahan terhadap DNS rebinding.
- Transport yang di-pin mengikat DNS dan TLS. Dengan response factory dan pin yang terkonfigurasi, bridge mengikat koneksi ke IP yang telah diverifikasi, memberlakukan pinning SPKI, memverifikasi peer dan host, serta menolak mengikuti redirect ke host yang belum diverifikasi.
- Input dibatasi. HTML yang melebihi
maxHtmlSize(default 5 MB), data URI base64 yang terlalu besar, dan tag<meta http-equiv="refresh">apa pun ditolak sebelum permintaan dikirim. - Rahasia diredaksi dan bersifat immutable.
apiTokendan kunci R2 membawa#[SensitiveParameter], sehingga stack trace meredaksi nilai tersebut, dan objek konfigurasi bersifatfinal readonly. Muat rahasia dari lingkungan atau pengelola rahasia; jangan pernah meng-commit rahasia tersebut. - Jangan pernah menulis blok
catchyang kosong. Setiap contoh menangkap tipe exception spesifik lalu mencatat log atau keluar dengan kode yang terdefinisi.
Model keamanan lengkap tersedia pada halaman security-and-operations Cloudflare di bagian Lihat juga. Halaman tersebut membahas pertahanan SSRF dan DNS-rebinding, operasi pinning, penanganan rahasia, serta klausa OWASP dan RFC 7469 yang relevan.
Konformansi
Bagian berjudul “Konformansi”Panduan ini tidak membuat klaim standar normatif tersendiri. Pada halaman security-and-operations dan konfigurasi Cloudflare upstream, resolusi DNS untuk seluruh record dan pemeriksaan ulang TOCTOU milik bridge dipetakan ke pedoman pencegahan SSRF OWASP, sedangkan public-key pinning TLS serta pemulihan pin cadangannya dipetakan ke RFC 7469. Halaman cookbook ini menyatakan ulang cara penggunaannya dan menyerahkan kutipan tersebut ke halaman-halaman itu. Bridge tidak melakukan penandatanganan dan tidak membuat klaim konformansi tanda tangan.
Lihat juga
Bagian berjudul “Lihat juga”- Render HTML menjadi PDF dengan renderer Chrome Artisan — renderer dalam proses yang digunakan sebagai fallback lokal di sini.
- Quickstart Cloudflare — render edge pertama Anda dan model hasilnya.
- Keamanan dan operasi Cloudflare — SSRF, DNS-rebinding, pinning, dan rotasi rahasia.
- Penggunaan produksi Cloudflare — menghubungkan fallback, telemetri, pengarsipan R2, dan proteksi API.