跳到內容

正式環境使用方式:後備、遙測、封存與保護

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