生产环境用法 — 回退、遥测、归档、保护
本页说明此套件在单纯渲染之外处理的四项生产环境考量:本地回退、边缘遥测、R2 归档,以及入站 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。本地路径不会报告边缘位置或测得的高度。桥接器会通过 widthPt 选项键,把请求的宽度传给本地渲染器。
回退决策逻辑
标题为“回退决策逻辑”的章节直接从 CloudflareHtmlRenderer 读取:
| 情境 | 结果 |
|---|---|
设定不完整、fallbackToLocal: false | CloudflareNotAvailableException |
设定不完整、fallbackToLocal: true、已接入工厂 | 本地渲染 |
| Worker 抛出传输错误、已启用回退、已接入工厂 | 本地渲染,先以 warning 再以 info 记录 |
| Worker 抛出例外、已启用回退、已安装 Artisan、无工厂 | CloudflareNotAvailableException,并指出缺少的工厂 |
| Worker 抛出例外、已启用回退、未安装 Artisan | CloudflareNotAvailableException,并指出缺少的套件 |
| Worker 回传 HTTP 错误/格式错误的主体 | CloudflareRenderException,绝不进行回退 |
最后一列是关键差异。Worker 返回错误响应属于渲染失败,而不是可达性失败。它会被重新抛出,让你的代码能够区分渲染故障与边缘无法连接。
边缘遥测
标题为“边缘遥测”的章节每一次成功的二进制路径渲染,都会带有由响应标头派生出的遥测数据:
$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(),]);渲染器会从 CF-Ray 响应标头推导 renderLocation,取最后一个连字符之后的片段。以 CF-Ray: 8abc123def456-TPE 为例,位置就是 TPE。当该标头不存在时,位置为空字符串。在 JSON 响应路径上,该值改为来自 JSON 的 renderLocation 字段。请将这些值视为来自 Worker 的可观测性信号,而不是平台保证。
R2 归档
标题为“R2 归档”的章节R2ArchiveManager 会通过兼容 S3 的 API 将 PDF 字节上传至 Cloudflare R2,并使用 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() 会创建一个使用 AWS Signature V4 查询字符串签署的 GET URL,其 X-Amz-Expires 由你控制(默认 3600 秒)。规范请求会使用 UNSIGNED-PAYLOAD 这个内容哈希哨兵值。以查询字符串签署的读取 URL 使用这种形式,是因为主体并不是签署请求的一部分。这里描述的是此套件已实现的签署行为,来自对 R2ArchiveManager 的读取。Amazon 的服务文档定义了 AWS Signature Version 4,这并非 SDO 标准,因此此处未固定任何规范条款。对象访问密钥标注为 #[SensitiveParameter];请勿将其写入日志。
公开 URL
标题为“公开 URL”的章节R2UploadResult::publicUrl($customDomain) 在未提供域名时返回纯键,否则返回 https://<domain>/<key>。当所提供的域名没有协议时,它会强制使用 HTTPS 协议。它并不会让私有存储桶变为公开。那是 R2 存储桶设置层面的事。
入站 API 保护
标题为“入站 API 保护”的章节ApiProtection 是你应用在渲染请求上的保护层,位置在请求抵达 Worker 前方的 PHP 网关。它会按固定顺序执行三项检查:先是 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()会比对 SHA-256 哈希,用于静态密钥存储。密钥参数带有#[SensitiveParameter]。- 速率限制存储是按进程划分的内存中存储。它会跟踪一个每分钟的窗口(
rateLimitWindowSeconds,默认60),以及一个每小时的窗口(固定3600秒)。它并不会在多个 worker 或重新启动之间持久保存。若要在多个进程之间共享限制,请在它前面放置一个共享存储。 ApiProtectionResult::toHeaders()一律会加上X-Content-Type-Options: nosniff与X-Frame-Options: DENY,并合并速率限制标头(X-RateLimit-Remaining、X-RateLimit-Reset,以及在被拒绝时加上Retry-After)。
先渲染再签署
标题为“先渲染再签署”的章节此桥接器不会签署 PDF。生产环境的签署流程会先在边缘渲染,再由引擎签署返回的字节:
render()→CloudflareRenderResult::$pdfData。- 将
$pdfData交给nextpdf/core(或使用 NextPDF Pro 进行 PAdES B-B 签署)。长期验证配置文件属于 Enterprise 功能;此核心桥接器不声明支持这两者。
请将签署步骤保留在你自己的进程中,确保签署密钥绝不跨越边缘边界。
另请参阅
标题为“另请参阅”的章节- /integrations/cloudflare/security-and-operations/ — 固定、SSRF 防御、机密轮替、运维手册。
- /integrations/cloudflare/troubleshooting/ — 失效模式目录。
- /integrations/cloudflare/configuration/ — 每个栏位与默认值。