Lewati ke konten

Penggunaan produksi — fallback, telemetri, pengarsipan, dan proteksi

Halaman ini membahas empat kebutuhan produksi selain render dasar: fallback lokal, telemetri edge, pengarsipan Cloudflare R2, dan lapisan proteksi application programming interface (API) untuk permintaan masuk. Setiap bagian merujuk pada perilaku kelas yang telah diverifikasi.

Ketika Worker tidak dapat dijangkau dan fallbackToLocal bernilai true, bridge mendelegasikan proses render ke renderer lokal. Sediakan renderer tersebut melalui LocalRendererFactoryInterface. Bridge membuatnya secara lazy, sehingga create() pada factory hanya berjalan pada jalur 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);
}
};
}
}

Hubungkan factory ke renderer:

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

Ketika fallback berjalan, renderLocation pada hasil adalah string literal local, dan heightPt bernilai 0.0. Jalur lokal tidak melaporkan lokasi edge atau tinggi yang terukur. Bridge meneruskan lebar yang diminta ke renderer lokal melalui key opsi widthPt.

Baca langsung dari CloudflareHtmlRenderer:

SituasiHasil
Konfigurasi tidak lengkap, fallbackToLocal: falseCloudflareNotAvailableException
Konfigurasi tidak lengkap, fallbackToLocal: true, factory terhubungRender lokal
Worker melempar transport error, fallback aktif, factory terhubungRender lokal, dicatat sebagai warning, lalu info
Worker melempar, fallback aktif, Artisan terpasang, tanpa factoryCloudflareNotAvailableException yang menyebutkan factory yang hilang
Worker melempar, fallback aktif, Artisan tidak terpasangCloudflareNotAvailableException yang menyebutkan paket yang hilang
Worker mengembalikan error Hypertext Transfer Protocol (HTTP) / body malformedCloudflareRenderException, tidak pernah melakukan fallback

Baris terakhir sangat penting. Error yang dikembalikan Worker adalah kegagalan render, bukan kegagalan keterjangkauan. Bridge melempar ulang error tersebut agar kode Anda dapat membedakan render yang rusak dari edge yang tidak dapat dijangkau.

Setiap render yang berhasil melalui jalur biner menyertakan telemetri dari header respons:

$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 membaca renderLocation dari header respons CF-Ray dan mengambil segmen setelah tanda hubung terakhir. Untuk CF-Ray: 8abc123def456-TPE, lokasinya adalah TPE. Jika header tidak ada, lokasinya berupa string kosong. Pada jalur respons JavaScript Object Notation (JSON), nilainya berasal dari field JSON renderLocation. Perlakukan nilai-nilai ini sebagai sinyal observabilitas dari Worker, bukan jaminan platform.

R2ArchiveManager mengunggah byte Portable Document Format (PDF) ke Cloudflare R2 melalui API yang kompatibel dengan Amazon Simple Storage Service (S3), lalu menandatangani permintaan dengan 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]);
}

Perilaku diverifikasi dari R2ArchiveManager dan R2ObjectKey:

  • Object key dipartisi berdasarkan tanggal dengan format: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, sebagai contoh invoices/2026/05/18/invoice-2026-0042.pdf.
  • Nama berkas disanitasi: basename() menghapus path traversal, lalu byte null dan karakter kontrol (\x00\x1f, \x7f) dibuang. Hasil kosong menjadi document.pdf.
  • Metadata khusus dikirim sebagai header x-amz-meta-<lowercased-key>, termasuk dalam himpunan signed-header V4.
  • Berkas yang lebih besar dari maxFileSizeBytes (standar 104857600) ditolak sebelum ada permintaan apa pun dan mengembalikan R2UploadResult dengan success: false.
  • R2UploadResult::isValid() mensyaratkan success, key yang tidak kosong, dan etag yang tidak kosong.

URL unduhan yang sudah ditandatangani (pre-signed)

Bagian berjudul “URL unduhan yang sudah ditandatangani (pre-signed)”
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() membangun URL GET yang ditandatangani melalui query dengan AWS Signature V4 beserta nilai X-Amz-Expires yang Anda kendalikan (standar 3600 detik). Permintaan kanonis menggunakan sentinel content-hash UNSIGNED-PAYLOAD. URL baca yang ditandatangani melalui query menggunakan bentuk ini karena body bukan bagian dari permintaan yang ditandatangani. Bagian ini menjelaskan perilaku penandatanganan yang diimplementasikan paket, sebagaimana dibaca dari R2ArchiveManager. Dokumentasi layanan Amazon mendefinisikan AWS Signature Version 4, bukan standar dari standards development organization (SDO), sehingga tidak ada klausa normatif yang diacu di sini. Kunci akses objek ditandai dengan #[SensitiveParameter]; jauhkan dari log.

R2UploadResult::publicUrl($customDomain) mengembalikan key polos ketika Anda tidak menyediakan domain, atau https://<domain>/<key> ketika Anda menyediakannya. Metode ini menambahkan skema Hypertext Transfer Protocol Secure (HTTPS) ketika domain yang diberikan belum memilikinya. Ini tidak menjadikan bucket privat sebagai publik; hal itu tetap menjadi urusan konfigurasi bucket R2.

ApiProtection adalah lapisan yang Anda terapkan pada permintaan render yang masuk ke gateway PHP di depan Worker. Lapisan ini memeriksa dengan urutan tetap: kunci API, lalu ukuran payload, lalu 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;
}

Perilaku yang diverifikasi:

  • Urutannya adalah kunci API → ukuran payload → rate limit. Pemeriksaan pertama yang gagal melakukan short-circuit dengan denialReason yang spesifik.
  • ApiKeyValidator::validate() menggunakan hash_equals() untuk perbandingan yang aman terhadap timing dan menolak kunci kosong. validateHashed() membandingkan terhadap hash Secure Hash Algorithm 256-bit (SHA-256) untuk penyimpanan kunci saat istirahat (at-rest). Parameter kunci ditandai dengan #[SensitiveParameter].
  • Store rate-limit bersifat dalam memori per proses. Store ini melacak jendela per menit (rateLimitWindowSeconds, standar 60) dan jendela per jam (tetap 3600 detik). Store ini tidak bertahan lintas worker atau restart. Untuk berbagi limit lintas proses, tempatkan store bersama di depannya.
  • ApiProtectionResult::toHeaders() selalu menambahkan X-Content-Type-Options: nosniff dan X-Frame-Options: DENY, dan menggabungkan header rate-limit (X-RateLimit-Remaining, X-RateLimit-Reset, ditambah Retry-After ketika ditolak).

Bridge ini tidak menandatangani PDF. Untuk membangun pipeline penandatanganan produksi, lakukan render di edge, lalu tandatangani byte yang dikembalikan dengan engine:

  1. render()CloudflareRenderResult::$pdfData.
  2. Serahkan $pdfData ke nextpdf/core (atau NextPDF Pro untuk penandatanganan PDF Advanced Electronic Signatures (PAdES) B-B). Profil long-term-validation adalah kapabilitas Enterprise; bridge core ini tidak mengklaim kapabilitas mana pun.

Jaga agar langkah penandatanganan tetap berada di dalam proses Anda sendiri sehingga kunci penandatangan tidak pernah melintasi batas edge.

  • /integrations/cloudflare/security-and-operations/ — pinning, pertahanan server-side request forgery (SSRF), rotasi rahasia, dan runbook operasional.
  • /integrations/cloudflare/troubleshooting/ — katalog mode kegagalan.
  • /integrations/cloudflare/configuration/ — setiap field dan nilai standarnya.