Перейти к содержимому

Продакшен-использование — резервная отрисовка, телеметрия, архивирование, защита

Эта страница описывает четыре аспекта продакшена помимо базовой отрисовки: локальную резервную отрисовку, телеметрию 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: falseCloudflareNotAvailableException
Конфигурация неполная, fallbackToLocal: true, фабрика подключенаЛокальная отрисовка
Worker выбрасывает транспортную ошибку, резервная отрисовка включена, фабрика подключенаЛокальная отрисовка, журналируется на уровне warning, затем info
Worker выбрасывает исключение, резервная отрисовка включена, Artisan установлен, фабрики нетCloudflareNotAvailableException с указанием отсутствующей фабрики
Worker выбрасывает исключение, резервная отрисовка включена, Artisan не установленCloudflareNotAvailableException с указанием отсутствующего пакета
Worker возвращает ошибку Hypertext Transfer Protocol (HTTP) / некорректное телоCloudflareRenderException, резервная отрисовка никогда не задействуется

Последняя строка критически важна. Если Worker возвращает ошибку, это сбой отрисовки, а не сбой доступности. Мост повторно выбрасывает её, чтобы ваш код мог отличить неудачную отрисовку от недоступного 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, а не как гарантии платформы.

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 = $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]; не допускайте их попадания в журналы.

R2UploadResult::publicUrl($customDomain) возвращает чистый ключ, если домен не указан, или https://<domain>/<key>, если вы его указали. Метод добавляет схему Hypertext Transfer Protocol Secure (HTTPS), если у переданного домена её нет. Он не делает приватный бакет публичным; это остаётся вопросом конфигурации бакета R2.

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, затем подпишите возвращённые байты в движке:

  1. render()CloudflareRenderResult::$pdfData.
  2. Передайте $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/ — каждое поле и значение по умолчанию.