Lewati ke konten

Referensi API Cloudflare

Paket NextPDF\Cloudflare menjembatani rendering edge. Proses PHP Anda menyimpan Hypertext Markup Language (HTML), sementara Cloudflare Worker menjalankan peramban headless. Paket ini menyediakan renderer HTML berbasis Worker (CloudflareHtmlRenderer) beserta objek nilai yang dikembalikannya, lapisan perlindungan permintaan untuk endpoint render (ApiProtection), layanan arsip R2 untuk berkas Portable Document Format (PDF) hasil render (R2ArchiveManager), dan helper transport ber-pin untuk pengerasan Transport Layer Security (TLS) dan Domain Name System (DNS). Konfigurasi berada dalam tiga objek imutabel (CloudflareRendererConfig, ApiProtectionConfig, R2ArchiveConfig).

Untuk memulai, buat CloudflareRendererConfig, hubungkan ke CloudflareHtmlRenderer, lalu panggil render(). Pemanggilan itu mengirim HTML Anda ke Worker dan mengembalikan CloudflareRenderResult berisi bita PDF. Perlindungan, pengarsipan, dan pinning semuanya melapisi pemanggilan tersebut.

Cuplikan kode berikut mencakup alur kerja yang paling mungkin Anda gunakan. Masing-masing berdiri sendiri, telah diverifikasi terhadap src/Cloudflare/, dan membaca rahasia dari environment.

Render string HTML menjadi PDF di edge dengan pemanggilan kanonis:

<?php
declare(strict_types=1);
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use NextPDF\Cloudflare\CloudflareHtmlRenderer;
use NextPDF\Cloudflare\CloudflareRendererConfig;
$httpFactory = new HttpFactory();
$renderer = new CloudflareHtmlRenderer(
config: new CloudflareRendererConfig(
workerUrl: 'https://pdf-renderer.example.workers.dev/render',
apiToken: getenv('CF_PDF_TOKEN') ?: throw new RuntimeException('CF_PDF_TOKEN not set'),
),
httpClient: new Client(),
requestFactory: $httpFactory,
streamFactory: $httpFactory,
responseFactory: $httpFactory,
);
$result = $renderer->render('<h1>Hello from the edge</h1>', widthPt: 595.28);
if ($result->isValid()) {
file_put_contents('output.pdf', $result->pdfData);
}

Apa yang dilakukannya: mengirim HTML ke Worker melalui Hypertext Transfer Protocol Secure (HTTPS), lalu menulis bita PDF A4 yang dikembalikan ke disk setelah isValid() mengonfirmasi bahwa PDF tersebut valid.

Arsipkan PDF hasil render ke R2 dan kembalikan tautan berumur singkat:

<?php
declare(strict_types=1);
use NextPDF\Cloudflare\R2ArchiveConfig;
use NextPDF\Cloudflare\R2ArchiveManager;
$archive = new R2ArchiveManager(
config: R2ArchiveConfig::fromArray([
'bucket_name' => 'pdf-archive',
'account_id' => getenv('CF_ACCOUNT_ID') ?: '',
'access_key_id' => getenv('R2_ACCESS_KEY_ID') ?: '',
'secret_access_key' => getenv('R2_SECRET_ACCESS_KEY') ?: '',
]),
httpClient: $httpClient, // PSR-18 ClientInterface
requestFactory: $requestFactory, // PSR-17 RequestFactoryInterface
streamFactory: $streamFactory, // PSR-17 StreamFactoryInterface
);
$upload = $archive->upload($result->pdfData, 'invoice-1234.pdf');
$signedUrl = $upload->isValid()
? $archive->generateSignedUrl($upload->key, expiresInSeconds: 600)
: null;

Apa yang dilakukannya: mengunggah bita PDF ke kunci R2 yang dipartisi berdasarkan tanggal dan, jika berhasil, membuat uniform resource locator (URL) pra-tanda tangan berdurasi 10 menit untuk unduhan sementara.

Lindungi endpoint render sebelum memulai pekerjaan Worker yang mahal:

<?php
declare(strict_types=1);
use NextPDF\Cloudflare\ApiKeyValidator;
use NextPDF\Cloudflare\ApiProtection;
use NextPDF\Cloudflare\ApiProtectionConfig;
$protection = new ApiProtection(
config: new ApiProtectionConfig(maxRequestsPerMinute: 30),
keyValidator: new ApiKeyValidator([getenv('RENDER_API_KEY') ?: '']),
);
$decision = $protection->checkRequest(
clientId: $clientIp,
payloadSize: strlen($html),
apiKey: $presentedApiKey,
);
if (!$decision->allowed) {
// Reject with 429 and rate-limit headers before any render call.
return [429, $decision->toHeaders(), $decision->denialReason];
}

Apa yang dilakukannya: memvalidasi kunci API dan ukuran payload, memeriksa batas laju per klien, lalu mengembalikan satu keputusan beserta header respons yang harus disertakan ketika permintaan ditolak.

Tabel ini mencakup permukaan inti renderer. Gunakan tabel ini saat Anda menyusun konfigurasi, membuat renderer, atau menjalankan pemanggilan render dan pemeriksaan keterjangkauan.

SimbolParameterPerilaku standarMengembalikanMelempar atau gagal denganCatatan
new CloudflareRendererConfig(string $workerUrl, string $apiToken, int $renderTimeout = 30, string $defaultCss = '', int $maxHtmlSize = 5000000, ?string $r2FontBucket = null, bool $fallbackToLocal = true, array $pinnedPublicKeys = [], array $backupPublicKeys = [])URL Worker, bearer token, timeout, Cascading Style Sheets (CSS), batas ukuran, R2 font bucket opsional, flag fallback, set pin.Fallback lokal diaktifkan; pinning nonaktif ketika array pin kosong.CloudflareRendererConfigTidak ada yang diharapkan.Jaga kerahasiaan token API; utamakan URL Worker HTTPS.
CloudflareRendererConfig::fromArray(array $config)worker_url, api_token, render_timeout, default_css, max_html_size, r2_font_bucket, fallback_to_local, array pin.Kunci opsional yang hilang memakai nilai standar konstruktor.CloudflareRendererConfigTidak ada yang diharapkan.Gunakan untuk array konfigurasi bergaya framework.
CloudflareRendererConfig::isValid()tidak ada.Membutuhkan URL Worker dan token API yang tidak kosong.boolTidak ada yang diharapkan.Konfigurasi yang tidak valid memicu fallback atau kegagalan pada renderer.
CloudflareRendererConfig::allPublicKeyPins()tidak ada.Menggabungkan pin kunci publik utama dan cadangan.list<string>Tidak ada yang diharapkan.Daftar kosong menonaktifkan pinning.
new CloudflareHtmlRenderer(CloudflareRendererConfig $config, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, ?LoggerInterface $logger = null, ?LocalRendererFactoryInterface $localRendererFactory = null, ?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null, ?ResponseFactoryInterface $responseFactory = null)Konfigurasi, dependensi PHP Standards Recommendation (PSR) Hypertext Transfer Protocol (HTTP), logger opsional, factory fallback lokal opsional, kebijakan HTML opsional, response factory opsional.Menggunakan DefaultHtmlSecurityPolicy ketika tidak ada kebijakan HTML yang disediakan.CloudflareHtmlRendererKesalahan wiring kontainer.Response factory mengaktifkan transport cURL ber-pin bila diperlukan.
CloudflareHtmlRenderer::render(string $html, float $widthPt = 595.28, float $heightPt = 0, array $fontFiles = [])HTML, lebar halaman, tinggi halaman, berkas fon di R2.Lebar A4, tinggi otomatis, tanpa berkas fon.CloudflareRenderResultCloudflareNotAvailableException, CloudflareRenderException, kegagalan validasi.Memvalidasi ukuran HTML dan URL worker sebelum input/output (I/O) jaringan.
CloudflareHtmlRenderer::getHtmlSecurityPolicy()tidak ada.Mengembalikan kebijakan lapisan parse yang dikonfigurasi.HtmlSecurityPolicyInterfaceTidak ada yang diharapkan.Gunakan bersama perlindungan endpoint dan validasi URL Worker.
CloudflareHtmlRenderer::isAvailable()tidak ada.Mengirim permintaan HEAD ke Worker ketika konfigurasi valid.boolMengembalikan false saat terjadi kesalahan.Gunakan untuk pemeriksaan kesiapan, bukan sebagai satu-satunya penjaga saat runtime.

Gunakan tabel ini untuk objek nilai permintaan dan hasil (CloudflareRenderResult, CloudflareRenderPayload), serta pemeriksaan statis lapisan transport yang memvalidasi HTML, URL Worker, dan pin DNS sebelum I/O jaringan.

SimbolParameterPerilaku standarMengembalikanMelempar atau gagal denganCatatan
new CloudflareRenderResult(string $pdfData, float $widthPt, float $heightPt, float $contentHeightPx = 0.0, string $renderLocation = '', float $renderTimeMs = 0.0)Bita PDF, lebar, tinggi, tinggi konten terukur, lokasi edge, waktu render.Metadata kosong ketika Worker tidak melaporkannya.CloudflareRenderResultTidak ada yang diharapkan.Biasanya dikembalikan oleh CloudflareResponseParser::parse().
CloudflareRenderResult::isValid()tidak ada.Memeriksa bita PDF yang tidak kosong dan diawali dengan header PDF.boolTidak ada yang diharapkan.Gunakan sebelum mengarsipkan atau meneruskan bita ke lapisan lain.
CloudflareRenderResult::size()tidak ada.Menghitung bita PDF yang dirender.intTidak ada yang diharapkan.Masukkan ke dalam logika kuota dan audit.
new CloudflareRenderPayload(string $html, float $widthPt, float $heightPt = 0, string $defaultCss = '', ?string $r2FontBucket = null, array $fontFiles = [])HTML, ukuran, CSS, R2 font bucket opsional, daftar berkas fon.Tinggi otomatis, tanpa CSS default, tanpa R2 font bucket, tanpa berkas fon.CloudflareRenderPayloadTidak ada yang diharapkan.Objek nilai payload permintaan.
CloudflareRenderPayload::toJson()tidak ada.Menserialkan HTML, ukuran, CSS, dan referensi fon sebagai JavaScript Object Notation (JSON) untuk Worker.stringKesalahan pengodean JSON.API payload permintaan tingkat rendah.
CloudflareResponseParser::parse(ResponseInterface $response, float $requestedWidthPt)Respons Worker dan lebar yang diminta.Menerima respons PDF biner dan respons JSON terstruktur.CloudflareRenderResultCloudflareRenderException untuk keluaran Worker yang gagal atau tidak valid.Parser utama yang digunakan oleh renderer.
CloudflareSecurityPolicy::validate(string $html, int $maxSize)Input HTML dan batas ukuran.Menerapkan kebijakan input HTML paket ini.voidEksepsi validasi.Pertahankan pemeriksaan input yang tidak tepercaya di luar batas Worker.
CloudflareSecurityPolicy::validateWorkerUrl(string $url)URL Worker.Mengurai dan memvalidasi tujuan.arrayEksepsi validasi.Memblokir bentuk endpoint yang tidak aman sebelum I/O jaringan.
CloudflareSecurityPolicy::assertPinsStillValid(string $host, array $pinnedIps)Host dan daftar IP yang di-pin.Memverifikasi pin DNS yang diharapkan.voidEksepsi validasi ketika pin sudah usang atau tidak valid.Gunakan selama pemeriksaan operasional untuk deployment yang memakai pin.

Gunakan tabel ini saat Anda melindungi endpoint render: validasi kunci API, pemeriksaan ukuran payload dan batas laju, serta objek hasil dan header yang dihasilkan.

SimbolParameterPerilaku standarMengembalikanMelempar atau gagal denganCatatan
new ApiProtection(ApiProtectionConfig $config, ?ApiKeyValidator $keyValidator = null, ?Closure $clock = null)Konfigurasi perlindungan, validator kunci opsional, clock opsional.Menggunakan waktu sistem ketika tidak ada clock yang disediakan.ApiProtectionTidak ada yang diharapkan.Suntikkan clock deterministik dalam pengujian.
ApiProtection::checkRequest(string $clientId, int $payloadSize, string $apiKey = '')Pengidentifikasi klien, ukuran payload, kunci API opsional.Kunci API kosong hanya diizinkan ketika konfigurasi tidak mewajibkan kunci.ApiProtectionResultTidak ada yang diharapkan.Memeriksa kunci API dan ukuran, kemudian batas laju.
ApiProtection::getRateLimit(string $clientId)Pengidentifikasi klien.Tidak mencatat suatu permintaan.RateLimitResultTidak ada yang diharapkan.Gunakan untuk menambahkan header batas laju.
new ApiKeyValidator(array $validKeys = [])Daftar kunci plaintext yang valid.Daftar kosong menolak semua kunci.ApiKeyValidatorTidak ada yang diharapkan.Simpan rahasia di luar kode dan isi melalui konfigurasi.
ApiKeyValidator::validate(string $key)Kunci mentah.Membandingkan dengan kunci plaintext yang dikonfigurasi menggunakan logika timing-safe.boolTidak ada yang diharapkan.Parameter ini bersifat sensitif; jangan mencatat kunci mentah ke log.
ApiKeyValidator::addKey(string $key)Kunci mentah.Menambahkan kunci ter-hash ke instance validator baru.selfTidak ada yang diharapkan.Perlakukan instance yang dikembalikan sebagai validator yang diperbarui.
ApiKeyValidator::revokeKey(string $key)Kunci mentah.Menghapus hash yang cocok dari instance validator baru.selfTidak ada yang diharapkan.Perlakukan instance yang dikembalikan sebagai validator yang diperbarui.
ApiKeyValidator::hashKey(string $key)Kunci mentah.Menghasilkan representasi hash yang disimpan.stringTidak ada yang diharapkan.Jangan menampilkan hash di log atau respons klien.
ApiKeyValidator::validateHashed(string $key, array $hashedKeys)Kunci mentah dan hash kandidat.Membandingkan dengan hash yang disediakan dalam waktu konstan.boolTidak ada yang diharapkan.Helper tingkat rendah untuk penyimpanan kunci kustom.
new ApiProtectionConfig(int $maxRequestsPerMinute = 60, int $maxRequestsPerHour = 1000, int $maxPayloadSizeBytes = 10485760, array $allowedOrigins = [], bool $requireApiKey = true, string $apiKeyHeader = 'X-Api-Key', int $rateLimitWindowSeconds = 60)Batas permintaan, batas payload, origin yang diizinkan, persyaratan kunci API, nama header, panjang jendela.60/minute, 1000/hour, payload 10 MiB, kunci API diwajibkan.ApiProtectionConfigTidak ada yang diharapkan.Bangun langsung dalam pengujian atau isi dengan fromArray().
ApiProtectionConfig::fromArray(array $data)max_requests_per_minute, max_requests_per_hour, max_payload_size_bytes, allowed_origins, require_api_key, api_key_header, rate_limit_window_seconds.Kunci yang hilang memakai nilai standar konstruktor.ApiProtectionConfigTidak ada yang diharapkan.Gunakan untuk pengisian konfigurasi framework.
ApiProtectionConfig::isValid()tidak ada.Membutuhkan batas positif serta nilai ukuran dan jendela yang koheren.boolTidak ada yang diharapkan.Validasi sebelum mengekspos sebuah endpoint.
new ApiProtectionResult(bool $allowed, string $denialReason = '', ?RateLimitResult $rateLimit = null)Keputusan, alasan penolakan, hasil batas laju opsional.Alasan penolakan kosong dan tanpa hasil batas laju.ApiProtectionResultTidak ada yang diharapkan.Dikembalikan oleh ApiProtection::checkRequest().
ApiProtectionResult::toHeaders()tidak ada.Memancarkan header batas laju ketika data laju tersedia.array<string, string>Tidak ada yang diharapkan.Sertakan pada respons Worker atau framework.
new RateLimitResult(bool $allowed, int $remainingRequests, int $retryAfterSeconds, string $clientId)Keputusan, sisa hitungan, jeda coba ulang, pengidentifikasi klien (ID).Tidak ada nilai standar.RateLimitResultTidak ada yang diharapkan.Hasil imutabel untuk satu pemeriksaan.
RateLimitResult::toHeaders()tidak ada.Memancarkan header sisa batas dan reset.array<string, string>Tidak ada yang diharapkan.Gunakan untuk observabilitas dan backoff klien.
new RateLimitEntry(string $clientId, int $requestCount = 0, int $windowStart = 0, int $hourlyCount = 0, int $hourlyWindowStart = 0)Client ID dan penghitung yang dapat diubah.Penghitung dimulai dari nol.RateLimitEntryTidak ada yang diharapkan.Objek pelacakan dalam memori.
RateLimitEntry::increment()tidak ada.Menaikkan penghitung dalam memori untuk satu client/window.voidTidak ada yang diharapkan.Helper tingkat rendah yang digunakan oleh ApiProtection.
RateLimitEntry::isExpired(int $windowSeconds)Panjang jendela dalam detik.Membandingkan dengan waktu saat ini.boolTidak ada yang diharapkan.Helper kedaluwarsa saat runtime.
RateLimitEntry::isExpiredAt(int $now, int $windowSeconds)Nilai clock dan panjang jendela.Membandingkan dengan nilai clock yang disediakan.boolTidak ada yang diharapkan.Helper pengujian deterministik.
RateLimitEntry::reset()tidak ada.Mereset hitungan dan waktu mulai jendela.voidTidak ada yang diharapkan.Digunakan ketika jendela baru dimulai.

Gunakan tabel ini saat Anda menyimpan PDF hasil render di Cloudflare R2: layanan arsip, tipe konfigurasi dan kunci objeknya, serta hasil unggah yang harus diperiksa sebelum Anda mengekspos URL.

SimbolParameterPerilaku standarMengembalikanMelempar atau gagal denganCatatan
new R2ArchiveManager(R2ArchiveConfig $config, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory)Konfigurasi R2 dan factories/client. HTTP PSR.Tidak ada pemanggilan jaringan selama konstruksi.R2ArchiveManagerKesalahan wiring kontainer.Layanan arsip utama.
R2ArchiveManager::upload(string $pdfData, string $filename, array $metadata = [])Bita PDF mentah, nama berkas asli, metadata string.Metadata kosong; kunci yang dipartisi berdasarkan tanggal.R2UploadResultMengembalikan hasil yang tidak berhasil pada kegagalan konfigurasi, ukuran, HTTP, atau transport.Tidak melempar eksepsi untuk kegagalan unggah biasa.
R2ArchiveManager::generateSignedUrl(string $key, int $expiresInSeconds = 3600)Kunci objek dan time to live (TTL) URL.URL bertanda tangan selama satu jam.stringKesalahan penandatanganan akibat konfigurasi yang tidak valid.Gunakan TTL singkat untuk PDF sensitif.
R2ArchiveManager::buildObjectKey(string $filename)Nama berkas asli.Menggunakan prefiks path yang dikonfigurasi dan tanggal saat ini.R2ObjectKeyTidak ada yang diharapkan.Gunakan untuk partisi arsip yang dapat diprediksi.
R2ArchiveManager::createPutRequest(R2ObjectKey $key, string $data, array $metadata = [])Kunci objek, bita mentah, metadata.Menandatangani permintaan PUT.RequestInterfaceKesalahan konstruksi permintaan.API tingkat rendah untuk transport kustom.
new R2ArchiveConfig(string $bucketName, string $accountId, string $accessKeyId, string $secretAccessKey, string $endpoint = '', string $pathPrefix = 'pdfs/', int $maxFileSizeBytes = 104857600)Bucket, account ID, kredensial, penggantian endpoint, prefiks kunci, ukuran objek maksimum.endpoint turunan, prefiks pdfs/, ukuran objek maksimum 100 MiB.R2ArchiveConfigInvalidArgumentException untuk nama bucket yang tidak valid.Perlakukan kredensial sebagai konfigurasi yang mengandung rahasia.
R2ArchiveConfig::fromArray(array $data)Account ID, bucket, kredensial, prefiks path, penggantian endpoint, ukuran maksimum.Nilai yang hilang memakai nilai standar konstruktor.R2ArchiveConfigNama bucket tidak valid ketika disediakan.Gunakan untuk pengisian konfigurasi aplikasi.
R2ArchiveConfig::isValid()tidak ada.Membutuhkan account, bucket, access key, dan secret key yang tidak kosong.boolTidak ada yang diharapkan.Konfigurasi yang tidak valid membuat unggahan gagal dengan hasil terstruktur.
R2ArchiveConfig::getEndpoint()tidak ada.Menggunakan endpoint eksplisit atau menurunkan endpoint Cloudflare R2 dari account ID.stringTidak ada yang diharapkan.Digunakan untuk konstruksi permintaan bertanda tangan.
new R2ObjectKey(string $key, string $bucket)Kunci objek lengkap dan bucket.Tanpa normalisasi.R2ObjectKeyTidak ada yang diharapkan.Biasanya dibuat oleh R2ObjectKey::generate().
R2ObjectKey::generate(string $prefix, string $filename, ?DateTimeInterface $date = null)Prefiks, nama berkas asli, tanggal opsional.Kunci objek yang dipartisi berdasarkan tanggal dan disanitasi.R2ObjectKeyTidak ada yang diharapkan.Suntikkan tanggal dalam pengujian untuk kunci yang deterministik.
R2ObjectKey::fullPath()tidak ada.Menggabungkan path partisi dan nama berkas objek.stringTidak ada yang diharapkan.Simpan nilai ini sebagai kunci objek.
new R2UploadResult(bool $success, string $key, string $etag = '', int $size = 0, string $error = '')Flag keberhasilan, kunci objek, entity tag (ETag), ukuran bita, pesan kesalahan.ETag kosong, ukuran nol, pesan kesalahan kosong.R2UploadResultTidak ada yang diharapkan.Dikembalikan oleh R2ArchiveManager::upload().
R2UploadResult::isValid()tidak ada.Valid ketika unggahan berhasil dan baik kunci maupun ETag ada.boolTidak ada yang diharapkan.Periksa sebelum mengekspos URL.
R2UploadResult::publicUrl(string $customDomain = '')Domain publik kustom opsional.Mengembalikan kunci objek apa adanya ketika tidak ada domain kustom yang disediakan.stringTidak ada yang diharapkan.Hindari URL publik untuk dokumen sensitif kecuali kebijakan mengizinkannya.

Gunakan tabel ini hanya untuk wiring tingkat rendah: pinning Internet Protocol (IP) dan SubjectPublicKeyInfo (SPKI) pada level cURL, ditambah kontrak local-renderer yang digunakan sebagai jalur fallback ketika Worker tidak dapat dijangkau.

SimbolParameterPerilaku standarMengembalikanMelempar atau gagal denganCatatan
new PinnedCurlTransport(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory, array $pinnedIps = [], array $pinnedPublicKeys = [], int $timeoutSeconds = 30)Factory PSR-17, IP yang di-pin, kunci publik yang di-pin, timeout.Tanpa pin dan timeout 30 detik.PinnedCurlTransportTidak ada yang diharapkan.Gunakan hanya ketika pinning pada level cURL diperlukan.
PinnedCurlTransport::sendRequest(RequestInterface $request)Permintaan PSR-7.Mengirim melalui cURL dengan timeout dan kontrol pinning yang dikonfigurasi.ResponseInterfaceEksepsi transport PSR-18.Gunakan hanya ketika klien HTTP framework tidak dapat menegakkan kebijakan pinning yang sama.
PinnedCurlTransport::buildCurlOptions(RequestInterface $request, string $host, int $port)Permintaan, host tujuan, port tujuan.Membangun array opsi cURL yang digunakan oleh sendRequest().arrayKesalahan permintaan yang tidak valid atau konfigurasi pin.Hook pengujian dan diagnostik tingkat rendah.
LocalRendererInterface::render(string $html, array $options = [])HTML dan opsi renderer.Hanya kontrak; implementasi menetapkan nilai standar.stringKesalahan render yang spesifik per implementasi.Digunakan sebagai fallback lokal ketika renderer Worker tidak tersedia.
LocalRendererFactoryInterface::create()tidak ada.Membuat sebuah implementasi renderer lokal.LocalRendererInterfaceKesalahan factory atau dependensi.Menjaga konstruksi renderer fallback tetap di luar CloudflareHtmlRenderer.
  • Perlakukan URL Worker sebagai batas jaringan. Validasi tujuan, ukuran, dan autentikasi sebelum melakukan render.
  • Gunakan hasil perlindungan API sebagai keluaran kebijakan, bukan sebagai alur kendali eksepsi.
  • Unggahan R2 mengembalikan hasil keberhasilan atau kesalahan yang terstruktur; tangani kedua jalur.