Bỏ qua để đến nội dung

Sử dụng trong môi trường production — dự phòng, telemetry, lưu trữ và bảo vệ

Trang này trình bày bốn vấn đề trong môi trường production bên cạnh thao tác render cơ bản: dự phòng cục bộ, telemetry tại edge, lưu trữ vào Cloudflare R2 và lớp bảo vệ application programming interface (API) cho lưu lượng đến. Mỗi phần tương ứng với hành vi của lớp đã được kiểm chứng.

Khi không kết nối được với Worker và fallbackToLocaltrue, bridge sẽ ủy quyền việc render cho một renderer cục bộ. Hãy cung cấp renderer đó thông qua LocalRendererFactoryInterface. Bridge tạo nó theo cơ chế lazy, nên create() của factory chỉ chạy trên đường dẫn dự phòng.

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

Gắn factory vào renderer như sau:

use NextPDF\Cloudflare\CloudflareHtmlRenderer;
$renderer = new CloudflareHtmlRenderer(
config: $config,
httpClient: $httpClient,
requestFactory: $httpFactory,
streamFactory: $httpFactory,
logger: $logger,
localRendererFactory: new ArtisanLocalRendererFactory($chrome),
responseFactory: $httpFactory,
);

Khi dự phòng chạy, renderLocation của kết quả là chuỗi nguyên văn local, và heightPt0.0. Đường dẫn cục bộ không báo cáo vị trí edge hoặc chiều cao đo được. Bridge truyền chiều rộng được yêu cầu cho renderer cục bộ qua khóa tùy chọn widthPt.

Đọc trực tiếp từ CloudflareHtmlRenderer:

Tình huốngKết quả
Cấu hình chưa đầy đủ, fallbackToLocal: falseCloudflareNotAvailableException
Cấu hình chưa đầy đủ, fallbackToLocal: true, đã gắn factoryRender cục bộ
Worker ném lỗi transport, dự phòng đã bật, đã gắn factoryRender cục bộ, được ghi log ở mức warning rồi info
Worker ném lỗi, dự phòng đã bật, Artisan đã được cài đặt, không có factoryCloudflareNotAvailableException nêu tên factory bị thiếu
Worker ném lỗi, dự phòng đã bật, Artisan chưa được cài đặtCloudflareNotAvailableException nêu tên package bị thiếu
Worker trả về lỗi Hypertext Transfer Protocol (HTTP) / phần thân không hợp lệCloudflareRenderException, không bao giờ dùng dự phòng

Hàng cuối cùng rất quan trọng. Một Worker trả về lỗi là lỗi render, không phải lỗi không kết nối được. Bridge ném lại lỗi đó để mã của bạn có thể phân biệt một lần render hỏng với một edge không kết nối được.

Mỗi lần render thành công qua đường dẫn nhị phân đều kèm telemetry từ các header phản hồi:

$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(),
]);

Renderer đọc renderLocation từ header phản hồi CF-Ray và lấy phần sau dấu gạch nối cuối cùng. Với CF-Ray: 8abc123def456-TPE thì vị trí là TPE. Khi không có header này, vị trí là một chuỗi rỗng. Trên đường dẫn phản hồi JavaScript Object Notation (JSON), giá trị này được lấy từ trường JSON renderLocation. Hãy xem các giá trị này là tín hiệu observability từ Worker, không phải cam kết của nền tảng.

R2ArchiveManager tải các byte Portable Document Format (PDF) lên Cloudflare R2 qua API tương thích với Amazon Simple Storage Service (S3) và ký các yêu cầu bằng 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]);
}

Hành vi được kiểm chứng từ R2ArchiveManagerR2ObjectKey:

  • Khóa đối tượng được phân vùng theo ngày ở dạng: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, ví dụ invoices/2026/05/18/invoice-2026-0042.pdf.
  • Tên tệp được làm sạch: basename() loại bỏ path traversal, sau đó các byte null và ký tự điều khiển (\x00\x1f, \x7f) bị loại bỏ. Nếu kết quả rỗng, tên sẽ trở thành document.pdf.
  • Metadata tùy chỉnh được gửi dưới dạng các header x-amz-meta-<lowercased-key> và được đưa vào tập header được ký V4.
  • Các tệp lớn hơn maxFileSizeBytes (mặc định 104857600) bị từ chối trước khi gửi bất kỳ yêu cầu nào và trả về một R2UploadResult với success: false.
  • R2UploadResult::isValid() yêu cầu success, một key không rỗng và một etag không rỗng.
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() dựng một URL GET được ký qua query bằng AWS Signature V4 với một giá trị X-Amz-Expires do bạn kiểm soát (mặc định 3600 giây). Canonical request dùng giá trị sentinel content-hash UNSIGNED-PAYLOAD. URL đọc được ký qua query dùng dạng này vì phần thân không thuộc về yêu cầu được ký. Phần này mô tả hành vi ký đã triển khai của package, dựa trên những gì đọc được từ R2ArchiveManager. Tài liệu dịch vụ của Amazon định nghĩa AWS Signature Version 4, không phải một tiêu chuẩn của standards development organization (SDO), nên không có điều khoản chuẩn tắc nào được ghim ở đây. Các khóa truy cập đối tượng được đánh dấu #[SensitiveParameter]; hãy giữ chúng ngoài các log.

R2UploadResult::publicUrl($customDomain) trả về khóa dạng thuần khi bạn không cung cấp domain, hoặc https://<domain>/<key> khi bạn cung cấp. Nó thêm scheme Hypertext Transfer Protocol Secure (HTTPS) khi domain được cung cấp không có scheme. Nó không biến một bucket riêng tư thành công khai; việc đó vẫn thuộc về cấu hình bucket R2.

ApiProtection là lớp bạn áp dụng cho các yêu cầu render đi vào một PHP gateway đặt trước Worker. Nó kiểm tra theo một thứ tự cố định: API key, rồi kích thước payload, rồi giới hạn tần suất.

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

Hành vi đã kiểm chứng:

  • Thứ tự là API key → kích thước payload → giới hạn tần suất. Bước kiểm tra đầu tiên thất bại sẽ dừng ngay với một denialReason cụ thể.
  • ApiKeyValidator::validate() dùng hash_equals() để so sánh an toàn về thời gian và từ chối khóa rỗng. validateHashed() so sánh với các hash Secure Hash Algorithm 256-bit (SHA-256) để lưu trữ khóa ở trạng thái nghỉ. Các tham số khóa mang #[SensitiveParameter].
  • Bộ lưu trữ giới hạn tần suất là trong bộ nhớ theo từng tiến trình. Nó theo dõi một cửa sổ theo phút (rateLimitWindowSeconds, mặc định 60) và một cửa sổ theo giờ (cố định 3600 giây). Nó không được lưu giữ xuyên qua nhiều worker hoặc các lần khởi động lại. Để chia sẻ các giới hạn giữa nhiều tiến trình, hãy đặt một bộ lưu trữ dùng chung phía trước nó.
  • ApiProtectionResult::toHeaders() luôn thêm X-Content-Type-Options: nosniffX-Frame-Options: DENY, đồng thời gộp thêm các header giới hạn tần suất (X-RateLimit-Remaining, X-RateLimit-Reset, cộng thêm Retry-After khi bị từ chối).

Bridge này không ký các tệp PDF. Để xây dựng một pipeline ký cho production, hãy render tại edge, rồi ký các byte trả về bằng engine:

  1. render()CloudflareRenderResult::$pdfData.
  2. Chuyển $pdfData cho nextpdf/core (hoặc NextPDF Pro để ký PDF Advanced Electronic Signatures (PAdES) B-B). Các profile long-term-validation là một khả năng của Enterprise; bridge core này không tuyên bố hỗ trợ khả năng nào trong hai khả năng đó.

Hãy giữ bước ký trong tiến trình riêng của bạn để khóa ký không bao giờ vượt qua ranh giới edge.

  • /integrations/cloudflare/security-and-operations/ — pinning, chống server-side request forgery (SSRF), xoay vòng secret và runbook vận hành.
  • /integrations/cloudflare/troubleshooting/ — danh mục các chế độ lỗi.
  • /integrations/cloudflare/configuration/ — mọi trường và giá trị mặc định.