正式環境使用方式:後備、遙測、封存與保護
本頁說明此套件在單純算繪之外涵蓋的四項正式環境考量:本機後備、邊緣遙測、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>。當提供的網域未帶有 URL scheme 時,它會強制使用 HTTPS scheme。它並不會讓私有儲存桶變成公開;那是 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/ — 每個欄位與預設值。