Продакшен-использование — резервная отрисовка, телеметрия, архивирование, защита
Эта страница описывает четыре аспекта продакшена помимо базовой отрисовки: локальную резервную отрисовку, телеметрию edge, архивирование в Cloudflare R2 и слой защиты входящих запросов application programming interface (API). Каждый раздел опирается на проверенное поведение классов.
Локальная резервная отрисовка
Заголовок раздела «Локальная резервная отрисовка»Когда Worker недоступен, а для fallbackToLocal задано true, мост делегирует отрисовку локальному рендереру. Передайте этот рендерер через LocalRendererFactoryInterface. Мост создаёт его лениво, поэтому метод фабрики create() выполняется только на пути резервной отрисовки.
<?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); } }; }}Подключите фабрику к рендереру:
use NextPDF\Cloudflare\CloudflareHtmlRenderer;
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory, logger: $logger, localRendererFactory: new ArtisanLocalRendererFactory($chrome), responseFactory: $httpFactory,);При резервной отрисовке поле результата renderLocation содержит строковый литерал local, а heightPt равно 0.0. Локальный путь не сообщает ни местоположение edge, ни измеренную высоту. Мост передаёт запрошенную ширину локальному рендереру через параметр widthPt.
Логика принятия решения о резервной отрисовке
Заголовок раздела «Логика принятия решения о резервной отрисовке»Напрямую из CloudflareHtmlRenderer:
| Ситуация | Результат |
|---|---|
Конфигурация неполная, fallbackToLocal: false | CloudflareNotAvailableException |
Конфигурация неполная, fallbackToLocal: true, фабрика подключена | Локальная отрисовка |
| Worker выбрасывает транспортную ошибку, резервная отрисовка включена, фабрика подключена | Локальная отрисовка, журналируется на уровне warning, затем info |
| Worker выбрасывает исключение, резервная отрисовка включена, Artisan установлен, фабрики нет | CloudflareNotAvailableException с указанием отсутствующей фабрики |
| Worker выбрасывает исключение, резервная отрисовка включена, Artisan не установлен | CloudflareNotAvailableException с указанием отсутствующего пакета |
| Worker возвращает ошибку Hypertext Transfer Protocol (HTTP) / некорректное тело | CloudflareRenderException, резервная отрисовка никогда не задействуется |
Последняя строка критически важна. Если Worker возвращает ошибку, это сбой отрисовки, а не сбой доступности. Мост повторно выбрасывает её, чтобы ваш код мог отличить неудачную отрисовку от недоступного edge.
Телеметрия с edge
Заголовок раздела «Телеметрия с edge»Каждая успешная отрисовка по бинарному пути включает телеметрию из заголовков ответа:
$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(),]);Рендерер читает renderLocation из заголовка ответа CF-Ray и берёт сегмент после последнего дефиса. Для CF-Ray: 8abc123def456-TPE местоположение — TPE. Если заголовка нет, местоположение — пустая строка. На пути ответа JavaScript Object Notation (JSON) значение вместо этого берётся из поля JSON renderLocation. Рассматривайте эти значения как сигналы наблюдаемости от Worker, а не как гарантии платформы.
Архивирование в R2
Заголовок раздела «Архивирование в R2»R2ArchiveManager загружает байты Portable Document Format (PDF) в Cloudflare R2 через API, совместимый с Amazon Simple Storage Service (S3), и подписывает запросы по схеме Amazon Web Services (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]);}Поведение проверено по R2ArchiveManager и R2ObjectKey:
- Ключ объекта секционирован по дате в виде:
<pathPrefix><Y>/<m>/<d>/<sanitized-filename>, напримерinvoices/2026/05/18/invoice-2026-0042.pdf. - Имя файла очищается:
basename()устраняет обход каталогов, затем удаляются нулевые байты и управляющие символы (\x00–\x1f,\x7f). Пустой результат становитсяdocument.pdf. - Пользовательские метаданные отправляются в виде заголовков
x-amz-meta-<lowercased-key>и включаются в набор подписанных заголовков V4. - Файлы крупнее
maxFileSizeBytes(по умолчанию104857600) отклоняются до отправки какого-либо запроса и возвращаютR2UploadResultсо значениемsuccess: false. R2UploadResult::isValid()требуетsuccess, непустойkeyи непустойetag.
Предподписанные URL для скачивания
Заголовок раздела «Предподписанные URL для скачивания»$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);generateSignedUrl() формирует предварительно подписанный URL GET по схеме AWS Signature V4 со значением X-Amz-Expires, которое вы задаёте (по умолчанию 3600 секунд). Канонический запрос использует контрольное значение хеша содержимого UNSIGNED-PAYLOAD. Предварительно подписанный URL для чтения использует эту форму, поскольку тело не входит в подписываемый запрос. Здесь описано поведение подписания, реализованное в пакете и считанное из R2ArchiveManager. AWS Signature Version 4 определена в сервисной документации Amazon, а не в стандарте standards development organization (SDO), поэтому здесь не закреплён какой-либо нормативный пункт. Ключи доступа к объектам помечены #[SensitiveParameter]; не допускайте их попадания в журналы.
Публичные URL
Заголовок раздела «Публичные URL»R2UploadResult::publicUrl($customDomain) возвращает чистый ключ, если домен не указан, или https://<domain>/<key>, если вы его указали. Метод добавляет схему Hypertext Transfer Protocol Secure (HTTPS), если у переданного домена её нет. Он не делает приватный бакет публичным; это остаётся вопросом конфигурации бакета R2.
Защита входящих запросов API
Заголовок раздела «Защита входящих запросов API»ApiProtection — это слой, который вы применяете к запросам на отрисовку, поступающим на PHP-шлюз перед Worker. Проверки идут в фиксированном порядке: ключ API, затем размер полезной нагрузки, затем ограничение частоты запросов.
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;}Проверенное поведение:
- Порядок такой: ключ API → размер полезной нагрузки → ограничение частоты запросов. Первая непройденная проверка сразу прерывает обработку с конкретным
denialReason. ApiKeyValidator::validate()используетhash_equals()для сравнения, устойчивого к тайминговым атакам, и отклоняет пустой ключ.validateHashed()сравнивает с хешами Secure Hash Algorithm 256-bit (SHA-256) для хранения ключей в состоянии покоя. Параметры с ключами помечены#[SensitiveParameter].- Хранилище ограничений частоты запросов находится в памяти каждого процесса. Оно отслеживает поминутное окно (
rateLimitWindowSeconds, по умолчанию60) и почасовое окно (фиксированные3600секунд). Оно не сохраняется между рабочими процессами или перезапусками. Чтобы разделять ограничения между процессами, разместите перед ним общее хранилище. ApiProtectionResult::toHeaders()всегда добавляетX-Content-Type-Options: nosniffиX-Frame-Options: DENYи объединяет заголовки ограничения частоты запросов (X-RateLimit-Remaining,X-RateLimit-Reset, а такжеRetry-Afterпри отказе).
Отрисовка, затем подписание
Заголовок раздела «Отрисовка, затем подписание»Этот мост не подписывает PDF. Чтобы построить продакшен-конвейер подписания, выполните отрисовку на edge, затем подпишите возвращённые байты в движке:
render()→CloudflareRenderResult::$pdfData.- Передайте
$pdfDataвnextpdf/core(или NextPDF Pro для подписания PDF Advanced Electronic Signatures (PAdES) B-B). Профили долгосрочной проверки (long-term validation) — это возможность Enterprise; этот мост core не заявляет ни одной из этих возможностей.
Оставьте шаг подписания в собственном процессе, чтобы ключ подписи никогда не пересекал границу edge.
См. также
Заголовок раздела «См. также»- /integrations/cloudflare/security-and-operations/ — закрепление (pinning), защита от server-side request forgery (SSRF), ротация секретов и операционный регламент.
- /integrations/cloudflare/troubleshooting/ — каталог режимов сбоев.
- /integrations/cloudflare/configuration/ — каждое поле и значение по умолчанию.