Отрисовка на периферии через Cloudflare с локальным резервным вариантом
Краткий обзор
Заголовок раздела «Краткий обзор»Мост Cloudflare отправляет ваш HTML на конечную точку отрисовки Cloudflare Worker и возвращает PDF. Отрисовка выполняется на периферии, поэтому вам не нужно поддерживать постоянно запущенный процесс браузера. Вы создаёте конфигурацию, допускающую только HTTPS, подключаете клиент PSR-18 и фабрики PSR-17, вызываете render() и можете добавить локальный отрисовщик на случай недоступности Workers. В этом руководстве показаны вызов отрисовки, сценарий резервного варианта, а также механизмы защиты от подделки запросов на стороне сервера (SSRF), перепривязки Domain Name System (DNS) и закрепления открытого ключа Transport Layer Security (TLS), которые мост применяет до того, как какой-либо запрос покинет процесс.
Сразу о предварительных требованиях:
- Установлены ядро NextPDF и
nextpdf/cloudflare. - Конечная точка Worker реализует контракт отрисовки по HTTPS и принимает токен типа bearer. Мост отклоняет URL Worker без HTTPS ещё до отправки любых данных.
- Доступны клиент PSR-18 (например, Guzzle 7), а также фабрики запросов и потоков PSR-17. Для транспорта cURL с закреплением также предоставьте фабрику ответов PSR-17 и
ext-curl. - Для локального резервного варианта доступен
nextpdf/artisan(или другой локальный отрисовщик).
Это практическое руководство. Если нужна первая рабочая отрисовка, начните с быстрого старта Cloudflare.
Установка
Заголовок раздела «Установка»Установите мост, клиент PSR-18 и фабрики PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzleДля локального резервного варианта установите локальный отрисовщик, который сможет вызывать мост.
composer require nextpdf/artisanЗагружайте токен типа bearer для Worker и любые учётные данные R2 из переменных окружения или менеджера секретов. Никогда не добавляйте их в систему контроля версий.
Концептуальный обзор
Заголовок раздела «Концептуальный обзор»CloudflareHtmlRenderer::render() проверяет HTML и назначение, отправляет аутентифицированный POST на Worker и обрабатывает ответ. Worker возвращает необработанные байты PDF (Content-Type: application/pdf) либо тело JSON с полем pdf в формате base64. Отрисовщик сопоставляет ответ с объектом final readonly CloudflareRenderResult, который содержит байты PDF, запрошенные ширину и высоту, место отрисовки (полученное из заголовка CF-Ray) и время отрисовки.
Мост разделяет сбои на два явных класса:
CloudflareRenderException— Worker ответил, но отрисовка не удалась (ошибка HTTP или тело, не начинающееся с%PDF). Это сбой отрисовки, и он никогда не повторяется через резервный вариант.CloudflareNotAvailableException— периферия недоступна, а пригодного резервного варианта не было.
Локальный резервный вариант покрывает второй случай. Когда Worker недоступен, а fallbackToLocal равно true, мост вызывает предоставленный вами LocalRendererFactoryInterface. Фабрика вызывается отложенно: create() выполняется только на пути резервного варианта. При отрисовке через резервный вариант renderLocation результата равен строковому литералу local.
Мост защищает сетевую границу до того, как какой-либо запрос покинет PHP. Он отклоняет URL Worker без HTTPS. Он отклоняет узел Worker, который разрешается в частное или зарезервированное адресное пространство, проверяя все записи A и AAAA, а не только первую. Он также повторно разрешает узел непосредственно перед подключением, что закрывает окно time-of-check/time-of-use (TOCTOU) для перепривязки DNS. Если вы предоставляете фабрику ответов PSR-17 и либо набор разрешённых IP-адресов, либо закрепления Subject Public Key Info (SPKI), мост использует транспорт cURL с закреплением. Этот транспорт привязывает соединение к проверенным IP-адресам (CURLOPT_RESOLVE), применяет закрепление открытого ключа TLS (CURLOPT_PINNEDPUBLICKEY), проверяет узел и имя хоста и не следует за перенаправлениями.
Поверхность API
Заголовок раздела «Поверхность 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() по умолчанию использует ширину A4 (595.28 пункта) и автоматически определяемую высоту (heightPt: 0). Полный справочник по полям и сопоставление ключей fromArray() см. на странице конфигурации Cloudflare в разделе “См. также”.
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»Создайте конфигурацию, создайте отрисовщик, выполните отрисовку и запишите байты.
<?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);}Токен берётся из окружения и никогда не прописывается жёстко в коде. workerUrl должен использовать HTTPS; мост отклоняет URL вида http:// до отправки любого запроса.
Пример кода — продакшен
Заголовок раздела «Пример кода — продакшен»В продакшене подключите фабрику локального отрисовщика, чтобы при недоступности Worker запрос переключался на резервный вариант, а не завершался сбоем. Настройте закрепления TLS и резервное закрепление. create() фабрики выполняется только на пути резервного варианта.
<?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(); } }; }}Передайте фабрику и закрепления отрисовщику.
<?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,);Когда срабатывает резервный вариант, renderLocation результата равен local, а значение heightPt равно 0.0. Мост журналирует переход на резервный вариант: сначала на уровне warning, затем на уровне info. Всегда настраивайте резервное закрепление до ротации сертификата, чтобы плановая ротация не отрезала мост от конечной точки.
Крайние случаи и подводные камни
Заголовок раздела «Крайние случаи и подводные камни»- Ошибка Worker — это не сбой доступности. Worker, который возвращает ошибку HTTP или некорректное тело, вызывает
CloudflareRenderExceptionи никогда не повторяется через резервный вариант. Переключение на резервный вариант выполняется только при недоступности периферии. Держите две ветви catch раздельными. - Резервному варианту нужны и флаг, и фабрика. При
fallbackToLocal: true, но без переданной фабрики недоступный Worker вызываетCloudflareNotAvailableExceptionи указывает на отсутствующую фабрику. Подключите фабрику. isAvailable()— это подсказка, а не гарантия. Метод отправляет аутентифицированныйHEADи возвращаетtrueдля статуса ниже500; следующийPOSTвсё равно может завершиться сбоем. Не рассматривайте его как контракт.- Закрепление необязательно. Пустой набор закреплений отключает закрепление. Используйте пустой набор только при стабильной, известной цепочке сертификатов, а при включённом закреплении держите резервное закрепление.
fontFilesтребует bucket R2. АргументfontFilesимеет значение только тогда, когда в конфигурации заданr2FontBucket; иначе он не действует.- Мост не выполняет подписание. Он возвращает байты PDF. Выполняйте отрисовку на периферии, а подписание — в собственном процессе, чтобы ключ подписания никогда не пересекал границу периферии.
Производительность
Заголовок раздела «Производительность»Отрисовка на периферии переносит браузерные затраты с ваших узлов. При этом вы по-прежнему оплачиваете один полный обмен по HTTPS с Worker плюс время отрисовки Worker, которое в результате указано как renderTimeMs. Мост применяет настроенный тайм-аут через транспорт с закреплением. Задавайте его с запасом относительно измеренной задержки Worker и держите ниже любого тайм-аута вышестоящего шлюза. Пакет сообщает только об ограничениях, которые применяет сам. Он не делает никаких утверждений о пределах платформы Cloudflare по CPU, памяти или размеру тела запроса. По этим ограничениям обращайтесь к документации Cloudflare и к вашему Worker.
Примечания по безопасности
Заголовок раздела «Примечания по безопасности»- Назначение проверяется до того, как запрос покинет PHP. URL-адреса без HTTPS отклоняются. Узел, который разрешается в частное или зарезервированное адресное пространство, отклоняется по всем записям A и AAAA. Узел повторно разрешается непосредственно перед подключением для защиты от перепривязки DNS.
- Транспорт с закреплением связывает DNS и TLS. Если настроены фабрика ответов и закрепления, мост привязывает соединение к проверенным IP-адресам, применяет закрепление SPKI, проверяет узел и имя хоста и отказывается следовать за перенаправлениями на непроверенный хост.
- Ввод ограничивается. HTML, превышающий
maxHtmlSize(по умолчанию 5 МБ), слишком большой data URI в формате base64 и любой тег<meta http-equiv="refresh">отклоняются до отправки запроса. - Секреты скрываются, конфигурация неизменяема.
apiTokenи ключи R2 помечены#[SensitiveParameter], поэтому они скрываются в трассировках стека, а объекты конфигурации объявлены какfinal readonly. Загружайте секреты из окружения или менеджера секретов; никогда не добавляйте их в систему контроля версий. - Никогда не пишите пустой блок
catch. Каждый пример перехватывает конкретный тип исключения и записывает его в журнал или завершается с определённым кодом.
Полная модель безопасности приведена на странице безопасности и эксплуатации Cloudflare в разделе “См. также”. Она охватывает защиту от SSRF и перепривязки DNS, операции закрепления, обработку секретов, а также соответствующие пункты OWASP и RFC 7469.
Соответствие
Заголовок раздела «Соответствие»Это руководство не содержит собственных нормативных заявлений о стандартах. На связанных страницах безопасности и эксплуатации, а также конфигурации Cloudflare указано, что разрешение DNS по всем записям и повторная проверка TOCTOU моста соответствуют рекомендациям OWASP по предотвращению SSRF, а закрепление открытого ключа TLS и восстановление через резервное закрепление соответствуют RFC 7469. Эта страница cookbook повторяет порядок действий и оставляет эти ссылки на упомянутых страницах. Мост не выполняет подписание и не делает заявлений о соответствии в области подписей.
См. также
Заголовок раздела «См. также»- Отрисовка HTML в PDF с помощью отрисовщика Artisan Chrome — отрисовщик внутри процесса, используемый здесь как локальный резервный вариант.
- Быстрый старт Cloudflare — первая отрисовка на периферии и модель результата.
- Безопасность и эксплуатация Cloudflare — SSRF, перепривязка DNS, закрепление и ротация секретов.
- Использование Cloudflare в продакшене — подключение резервного варианта, телеметрия, архивация в R2 и защита API.