ข้ามไปยังเนื้อหา

การใช้งานระดับ Production — fallback, การวัดและส่งข้อมูล (telemetry), การจัดเก็บถาวร และการป้องกัน

หน้านี้ครอบคลุมประเด็นด้าน Production สี่เรื่องที่อยู่นอกเหนือการ render พื้นฐาน ได้แก่ fallback ภายในเครื่อง การวัดและส่งข้อมูล (telemetry) จาก edge การจัดเก็บถาวรบน Cloudflare R2 และเลเยอร์การป้องกัน application programming interface (API) สำหรับคำขอขาเข้า แต่ละหัวข้อสอดคล้องกับพฤติกรรมของคลาสที่ตรวจสอบยืนยันแล้ว

เมื่อไม่สามารถเข้าถึง Worker ได้และ fallbackToLocal เป็น true bridge จะมอบหมายการ render ให้ renderer ภายในเครื่อง จัดเตรียม renderer นี้ผ่าน LocalRendererFactoryInterface bridge จะสร้าง renderer แบบ lazy ดังนั้น create() ของ factory จะทำงานเฉพาะบนเส้นทาง fallback เท่านั้น

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

เชื่อมต่อ factory เข้ากับ renderer:

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

เมื่อ fallback ทำงาน renderLocation ของผลลัพธ์จะเป็นสตริงตามตัวอักษร local และ heightPt จะเป็น 0.0 เส้นทางภายในเครื่องจะไม่รายงานตำแหน่ง edge หรือความสูงที่วัดได้ bridge จะส่งความกว้างที่ร้องขอไปยัง renderer ภายในเครื่องผ่านคีย์ตัวเลือก widthPt

อ่านโดยตรงจาก CloudflareHtmlRenderer:

สถานการณ์ผลลัพธ์
การตั้งค่าไม่สมบูรณ์ fallbackToLocal: falseCloudflareNotAvailableException
การตั้งค่าไม่สมบูรณ์ fallbackToLocal: true เชื่อมต่อ factory แล้วrender ภายในเครื่อง
Worker โยน transport error โดยเปิดใช้งาน fallback และเชื่อมต่อ factory แล้วrender ภายในเครื่อง บันทึก log ที่ระดับ warning จากนั้น info
Worker โยน error โดยเปิดใช้งาน fallback ติดตั้ง Artisan แล้ว แต่ ไม่มี factoryCloudflareNotAvailableException ที่ระบุชื่อ factory ที่ขาดหายไป
Worker โยน error โดยเปิดใช้งาน fallback แต่ ไม่ได้ ติดตั้ง ArtisanCloudflareNotAvailableException ที่ระบุชื่อแพ็กเกจที่ขาดหายไป
Worker คืนค่า Hypertext Transfer Protocol (HTTP) error / body ที่ผิดรูปแบบCloudflareRenderException ไม่ ทำ fallback เลย

แถวสุดท้ายเป็นจุดสำคัญ Worker ที่คืนค่า error ถือเป็นความล้มเหลวในการ render ไม่ใช่ความล้มเหลวด้านการเข้าถึง bridge จะโยน error นั้นซ้ำเพื่อให้โค้ดของคุณแยกแยะระหว่าง render ที่เสียหายกับ edge ที่ไม่สามารถเข้าถึงได้

การ render ที่สำเร็จทุกครั้งบนเส้นทาง binary จะมีข้อมูล telemetry จาก response header:

$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 จะอ่าน renderLocation จาก response header CF-Ray และใช้ส่วนที่อยู่หลังเครื่องหมายยัติภังค์ตัวสุดท้าย สำหรับ CF-Ray: 8abc123def456-TPE ตำแหน่งคือ TPE หากไม่มี header นี้ ตำแหน่งจะเป็นสตริงว่าง สำหรับเส้นทาง response แบบ JavaScript Object Notation (JSON) ค่าจะมาจากฟิลด์ renderLocation ใน JSON แทน ให้ถือว่าค่าเหล่านี้เป็นสัญญาณ observability จาก Worker ไม่ใช่การรับประกันของแพลตฟอร์ม

R2ArchiveManager อัปโหลดไบต์ของ Portable Document Format (PDF) ไปยัง Cloudflare R2 ผ่าน API ที่รองรับ Amazon Simple Storage Service (S3) และลงนามคำขอด้วย 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]);
}

พฤติกรรมที่ตรวจสอบยืนยันแล้วจาก R2ArchiveManager และ R2ObjectKey:

  • ระบบจะแบ่งพาร์ติชัน object key ตามวันที่ในรูปแบบ: <pathPrefix><Y>/<m>/<d>/<sanitized-filename> ตัวอย่างเช่น invoices/2026/05/18/invoice-2026-0042.pdf
  • ชื่อไฟล์จะถูก sanitize: basename() จะลบ path traversal ออก จากนั้น null byte และอักขระควบคุม (\x00\x1f, \x7f) จะถูกตัดออก หากผลลัพธ์ว่างเปล่า จะใช้ชื่อ document.pdf แทน
  • เมตาดาทาที่กำหนดเองจะถูกส่งในรูปแบบ header x-amz-meta-<lowercased-key> และรวมอยู่ในชุด signed-header ของ V4
  • ไฟล์ที่มีขนาดใหญ่กว่า maxFileSizeBytes (ค่าเริ่มต้น 104857600) จะถูกปฏิเสธก่อนส่งคำขอใด ๆ และคืนค่า R2UploadResult ที่มี success: false
  • R2UploadResult::isValid() กำหนดให้ต้องมี success key ที่ไม่ว่าง และ etag ที่ไม่ว่าง
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() สร้าง URL แบบ GET ที่ลงนามผ่าน query ด้วย AWS Signature V4 พร้อมค่า X-Amz-Expires ที่คุณควบคุมได้ (ค่าเริ่มต้น 3600 วินาที) canonical request ใช้ค่า sentinel ของ content-hash แบบ UNSIGNED-PAYLOAD URL สำหรับอ่านที่ลงนามผ่าน query ใช้รูปแบบนี้เพราะ body ไม่ได้เป็นส่วนหนึ่งของคำขอที่ลงนาม เนื้อหาส่วนนี้อธิบายพฤติกรรมการลงนามที่แพ็กเกจนำไปใช้งานจริงตามที่อ่านได้จาก R2ArchiveManager เอกสารบริการของ Amazon เป็นผู้กำหนด AWS Signature Version 4 ไม่ใช่มาตรฐานจาก standards development organization (SDO) จึงไม่มีการอ้างอิงข้อกำหนดเชิงบรรทัดฐานไว้ที่นี่ access key ของ object เป็น #[SensitiveParameter] อย่าให้ปรากฏใน log

R2UploadResult::publicUrl($customDomain) จะคืนค่าเป็นสตริงว่างเมื่อคุณไม่ระบุโดเมน หรือ https://<domain>/<key> เมื่อคุณระบุ เมธอดจะเพิ่ม scheme แบบ Hypertext Transfer Protocol Secure (HTTPS) เมื่อโดเมนที่ระบุไม่มี scheme เมธอดนี้ไม่ได้ทำให้ bucket ส่วนตัวกลายเป็นสาธารณะ ซึ่งยังคงเป็นเรื่องของการตั้งค่า R2 bucket

ApiProtection คือเลเยอร์ที่คุณนำมาใช้กับคำขอ render ที่เข้ามายัง PHP gateway หน้า Worker เลเยอร์นี้ตรวจสอบตามลำดับที่กำหนดไว้ตายตัว: API key จากนั้นขนาด payload จากนั้น rate limit

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 key → ขนาด payload → rate limit การตรวจสอบรายการแรกที่ล้มเหลวจะหยุดทันทีพร้อมระบุ denialReason ที่เฉพาะเจาะจง
  • ApiKeyValidator::validate() ใช้ hash_equals() สำหรับการเปรียบเทียบแบบ timing-safe และปฏิเสธคีย์ที่ว่างเปล่า validateHashed() เปรียบเทียบกับแฮช Secure Hash Algorithm 256-bit (SHA-256) สำหรับการจัดเก็บคีย์แบบ at-rest พารามิเตอร์ของคีย์มี #[SensitiveParameter] กำกับไว้
  • store สำหรับ rate-limit เป็นแบบ in-memory ต่อโปรเซส store จะติดตามหน้าต่างแบบต่อนาที (rateLimitWindowSeconds ค่าเริ่มต้น 60) และหน้าต่างแบบต่อชั่วโมง (คงที่ 3600 วินาที) store จะไม่คงอยู่ข้าม worker หรือการ restart หากต้องการแชร์ลิมิตข้ามโปรเซส ให้วาง shared store ไว้ด้านหน้า
  • ApiProtectionResult::toHeaders() จะเพิ่ม X-Content-Type-Options: nosniff และ X-Frame-Options: DENY เสมอ และผสาน header ของ rate-limit (X-RateLimit-Remaining, X-RateLimit-Reset รวมถึง Retry-After เมื่อถูกปฏิเสธ)

bridge นี้ไม่ลงนาม PDF หากต้องการสร้าง pipeline การลงนามระดับ Production ให้ render ที่ edge จากนั้นลงนามไบต์ที่ได้รับกลับมาด้วย engine:

  1. render()CloudflareRenderResult::$pdfData (ผลลัพธ์)
  2. ส่ง $pdfData ให้กับ nextpdf/core (หรือ NextPDF Pro สำหรับการลงนาม PDF Advanced Electronic Signatures (PAdES) B-B) โปรไฟล์ long-term-validation เป็นความสามารถระดับ Enterprise ส่วน core bridge นี้ไม่ได้อ้างว่ารองรับความสามารถทั้งสองอย่าง

เก็บขั้นตอนการลงนามไว้ในโปรเซสของคุณเอง เพื่อไม่ให้คีย์สำหรับลงนามข้ามขอบเขตของ edge เลย

  • /integrations/cloudflare/security-and-operations/ — การทำ pinning การป้องกัน server-side request forgery (SSRF) การหมุนเวียน secret และคู่มือการปฏิบัติงาน
  • /integrations/cloudflare/troubleshooting/ — แคตตาล็อกของรูปแบบความล้มเหลว
  • /integrations/cloudflare/configuration/ — ทุกฟิลด์และค่าเริ่มต้น