跳转到内容

生产环境用法 — 回退、遥测、归档、保护

本页说明此套件在单纯渲染之外处理的四项生产环境考量:本地回退、边缘遥测、R2 归档,以及入站 API 保护层。每个小节都对应已验证的类行为。

当无法连接到 Worker 且 fallbackToLocaltrue 时,桥接器会委派给本地渲染器。通过 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,且 heightPt0.0。本地路径不会报告边缘位置或测得的高度。桥接器会通过 widthPt 选项键,把请求的宽度传给本地渲染器。

直接从 CloudflareHtmlRenderer 读取:

情境结果
设定不完整、fallbackToLocal: falseCloudflareNotAvailableException
设定不完整、fallbackToLocal: true、已接入工厂本地渲染
Worker 抛出传输错误、已启用回退、已接入工厂本地渲染,先以 warning 再以 info 记录
Worker 抛出例外、已启用回退、已安装 Artisan、无工厂CloudflareNotAvailableException,并指出缺少的工厂
Worker 抛出例外、已启用回退、安装 ArtisanCloudflareNotAvailableException,并指出缺少的套件
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 的可观测性信号,而不是平台保证。

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]);
}

以下行为已从 R2ArchiveManagerR2ObjectKey 验证:

  • 对象键以日期分区:<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() 会创建一个使用 AWS Signature V4 查询字符串签署的 GET URL,其 X-Amz-Expires 由你控制(默认 3600 秒)。规范请求会使用 UNSIGNED-PAYLOAD 这个内容哈希哨兵值。以查询字符串签署的读取 URL 使用这种形式,是因为主体并不是签署请求的一部分。这里描述的是此套件已实现的签署行为,来自对 R2ArchiveManager 的读取。Amazon 的服务文档定义了 AWS Signature Version 4,这并非 SDO 标准,因此此处未固定任何规范条款。对象访问密钥标注为 #[SensitiveParameter];请勿将其写入日志。

R2UploadResult::publicUrl($customDomain) 在未提供域名时返回纯键,否则返回 https://<domain>/<key>。当所提供的域名没有协议时,它会强制使用 HTTPS 协议。它并不会让私有存储桶变为公开。那是 R2 存储桶设置层面的事。

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: nosniffX-Frame-Options: DENY,并合并速率限制标头(X-RateLimit-RemainingX-RateLimit-Reset,以及在被拒绝时加上 Retry-After)。

此桥接器不会签署 PDF。生产环境的签署流程会先在边缘渲染,再由引擎签署返回的字节:

  1. render()CloudflareRenderResult::$pdfData
  2. $pdfData 交给 nextpdf/core(或使用 NextPDF Pro 进行 PAdES B-B 签署)。长期验证配置文件属于 Enterprise 功能;此核心桥接器不声明支持这两者。

请将签署步骤保留在你自己的进程中,确保签署密钥绝不跨越边缘边界。

  • /integrations/cloudflare/security-and-operations/ — 固定、SSRF 防御、机密轮替、运维手册。
  • /integrations/cloudflare/troubleshooting/ — 失效模式目录。
  • /integrations/cloudflare/configuration/ — 每个栏位与默认值。