콘텐츠로 이동

Cloudflare API 레퍼런스

NextPDF\Cloudflare 패키지는 에지 렌더링 브리지입니다. PHP 프로세스는 HTML을 갖고, Cloudflare Worker는 헤드리스 브라우저를 갖습니다. 이 패키지는 Worker 기반 HTML 렌더러(CloudflareHtmlRenderer)와 해당 렌더러가 반환하는 값 객체, 렌더 엔드포인트를 보호하는 요청 보호 계층(ApiProtection), 렌더링된 PDF를 저장하는 R2 아카이브 서비스(R2ArchiveManager), 그리고 TLS/DNS 강화를 위한 고정 전송 헬퍼를 제공합니다. 구성은 세 개의 불변 객체(CloudflareRendererConfig, ApiProtectionConfig, R2ArchiveConfig)에 담깁니다.

먼저 여기서 시작하세요. 이 패키지가 처음이라면 CloudflareRendererConfig를 빌드하고 이를 CloudflareHtmlRenderer에 연결한 다음 render()를 호출하세요. 이 한 번의 호출이 HTML을 Worker로 전송하고 PDF 바이트가 담긴 CloudflareRenderResult를 반환합니다. 나머지 기능(보호, 아카이브, 고정)은 그 한 번의 호출을 중심으로 계층화됩니다.

아래 스니펫은 이 패키지에서 가장 자주 쓰이는 실제 워크플로입니다. 각 스니펫은 자체 완결적이고, src/Cloudflare/ 소스 기준으로 검증되었으며, 비밀 값은 환경에서 읽습니다.

HTML 문자열을 에지에서 PDF로 렌더링합니다. 단일 정식 렌더 호출입니다.

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

동작: HTML을 HTTPS를 통해 Worker로 전송하고, isValid()로 실제 PDF임을 확인한 뒤 반환된 A4 PDF 바이트를 디스크에 씁니다.

렌더링된 PDF를 R2에 아카이브하고 짧은 유효 기간의 링크를 반환합니다.

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

동작: PDF 바이트를 날짜로 분할된 R2 키에 업로드하고, 성공 시 임시 다운로드를 위한 10분 유효의 사전 서명 URL을 생성합니다.

비용이 큰 Worker 작업을 시작하기 전에 렌더 엔드포인트를 보호합니다.

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

동작: API 키와 페이로드 크기를 검증한 다음 클라이언트별 속도 제한을 확인하고, 요청이 거부될 때 첨부할 응답 헤더와 단일 결정을 반환합니다.

렌더러 표는 핵심 API 표면입니다. 구성을 작성하거나 렌더러를 빌드하거나 렌더링 및 도달 가능성 호출을 수행할 때 사용하세요.

심볼매개변수기본 동작반환값발생시키거나 실패하는 조건참고
new CloudflareRendererConfig(string $workerUrl, string $apiToken, int $renderTimeout = 30, string $defaultCss = '', int $maxHtmlSize = 5000000, ?string $r2FontBucket = null, bool $fallbackToLocal = true, array $pinnedPublicKeys = [], array $backupPublicKeys = [])Worker URL, 베어러 토큰, 타임아웃, CSS, 크기 제한, 선택적 R2 글꼴 버킷, 폴백 플래그, 고정 세트입니다.로컬 폴백이 활성화되어 있으며, 고정 배열이 비어 있으면 고정이 비활성화됩니다.CloudflareRendererConfig예상되지 않습니다.API 토큰은 비밀로 관리하고, HTTPS Worker URL을 선호하세요.
CloudflareRendererConfig::fromArray(array $config)worker_url, api_token, render_timeout, default_css, max_html_size, r2_font_bucket, fallback_to_local, 고정 배열입니다.선택적 키가 없으면 생성자 기본값을 사용합니다.CloudflareRendererConfig예상되지 않습니다.프레임워크 스타일 구성 배열에 맞습니다.
CloudflareRendererConfig::isValid()없음.비어 있지 않은 Worker URL과 API 토큰이 필요합니다.bool예상되지 않습니다.유효하지 않은 구성은 렌더러에서 폴백을 수행하거나 실패하게 만듭니다.
CloudflareRendererConfig::allPublicKeyPins()없음.기본 및 백업 공개 키 고정을 결합합니다.list<string>예상되지 않습니다.빈 목록은 고정을 비활성화합니다.
new CloudflareHtmlRenderer(CloudflareRendererConfig $config, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, ?LoggerInterface $logger = null, ?LocalRendererFactoryInterface $localRendererFactory = null, ?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null, ?ResponseFactoryInterface $responseFactory = null)구성, PSR HTTP 의존성, 선택적 로거, 선택적 로컬 폴백 팩토리, 선택적 HTML 정책, 선택적 응답 팩토리입니다.HTML 정책이 제공되지 않으면 DefaultHtmlSecurityPolicy를 사용합니다.CloudflareHtmlRenderer컨테이너 연결 오류입니다.응답 팩토리는 필요할 때 고정 cURL 전송을 활성화합니다.
CloudflareHtmlRenderer::render(string $html, float $widthPt = 595.28, float $heightPt = 0, array $fontFiles = [])HTML, 페이지 너비, 페이지 높이, R2의 글꼴 파일입니다.A4 너비와 자동 높이를 사용하며, 글꼴 파일은 없습니다.CloudflareRenderResultCloudflareNotAvailableException, CloudflareRenderException, 검증 실패입니다.네트워크 I/O 전에 HTML 크기와 Worker URL을 검증합니다.
CloudflareHtmlRenderer::getHtmlSecurityPolicy()없음.구성된 파싱 계층 정책을 반환합니다.HtmlSecurityPolicyInterface예상되지 않습니다.엔드포인트 보호 및 Worker URL 검증과 함께 사용하세요.
CloudflareHtmlRenderer::isAvailable()없음.구성이 유효할 때 Worker로 HEAD 요청을 보냅니다.bool오류가 발생하면 false를 반환합니다.준비 상태 확인용으로 사용하되, 유일한 런타임 가드로 사용하지 마세요.

request/result 값 객체(CloudflareRenderResult, CloudflareRenderPayload)가 필요하거나, 네트워크 I/O 전에 HTML, Worker URL, DNS 고정을 검증하는 정적 전송 계층 검사가 필요할 때 이 표를 사용하세요.

심볼매개변수기본 동작반환값발생시키거나 실패하는 조건참고
new CloudflareRenderResult(string $pdfData, float $widthPt, float $heightPt, float $contentHeightPx = 0.0, string $renderLocation = '', float $renderTimeMs = 0.0)PDF 바이트, 너비, 높이, 측정된 콘텐츠 높이, 에지 위치, 렌더 시간입니다.Worker가 보고하지 않으면 메타데이터는 비어 있습니다.CloudflareRenderResult예상되지 않습니다.일반적으로 CloudflareResponseParser::parse()가 반환합니다.
CloudflareRenderResult::isValid()없음.PDF 헤더로 시작하는 비어 있지 않은 PDF 바이트인지 확인합니다.bool예상되지 않습니다.아카이브하거나 바이트를 다른 계층에 전달하기 전에 사용하세요.
CloudflareRenderResult::size()없음.렌더링된 PDF 바이트 수를 셉니다.int예상되지 않습니다.할당량 및 감사 로직의 입력으로 사용하세요.
new CloudflareRenderPayload(string $html, float $widthPt, float $heightPt = 0, string $defaultCss = '', ?string $r2FontBucket = null, array $fontFiles = [])HTML, 크기, CSS, 선택적 R2 글꼴 버킷, 글꼴 파일 목록입니다.자동 높이와 기본 CSS를 사용하며, R2 글꼴 버킷과 글꼴 파일은 없습니다.CloudflareRenderPayload예상되지 않습니다.요청 페이로드 값 객체입니다.
CloudflareRenderPayload::toJson()없음.Worker용으로 HTML, 크기, CSS, 글꼴 참조를 직렬화합니다.stringJSON 인코딩 오류입니다.저수준 요청 페이로드 API입니다.
CloudflareResponseParser::parse(ResponseInterface $response, float $requestedWidthPt)Worker 응답과 요청된 너비입니다.바이너리 PDF 응답과 구조화된 JSON 응답을 수용합니다.CloudflareRenderResultWorker 출력이 실패했거나 유효하지 않으면 CloudflareRenderException이 발생합니다.렌더러가 사용하는 중앙 파서입니다.
CloudflareSecurityPolicy::validate(string $html, int $maxSize)HTML 입력과 크기 제한입니다.패키지 HTML 입력 정책을 적용합니다.void검증 예외입니다.신뢰할 수 없는 입력 검사는 Worker 경계 외부에서 유지하세요.
CloudflareSecurityPolicy::validateWorkerUrl(string $url)Worker URL 입니다.대상을 파싱하고 검증합니다.array검증 예외입니다.네트워크 I/O 전에 안전하지 않은 엔드포인트 형식을 차단합니다.
CloudflareSecurityPolicy::assertPinsStillValid(string $host, array $pinnedIps)호스트와 고정 IP 목록입니다.DNS 고정 기대값을 확인합니다.void고정이 오래되었거나 유효하지 않으면 검증 예외가 발생합니다.고정 배포의 운영 점검 시 사용하세요.

렌더 엔드포인트를 보호할 때 이 표를 사용하세요. API 키 검증, 페이로드 크기 및 속도 제한 검사, 그리고 이 과정에서 생성되는 result/header 객체를 다룹니다.

심볼매개변수기본 동작반환값발생시키거나 실패하는 조건참고
new ApiProtection(ApiProtectionConfig $config, ?ApiKeyValidator $keyValidator = null, ?Closure $clock = null)보호 구성, 선택적 키 검증기, 선택적 클럭입니다.클럭이 제공되지 않으면 시스템 시간을 사용합니다.ApiProtection예상되지 않습니다.테스트에서는 결정적 클럭을 주입하세요.
ApiProtection::checkRequest(string $clientId, int $payloadSize, string $apiKey = '')클라이언트 식별자, 페이로드 크기, 선택적 API 키입니다.빈 API 키는 구성이 키를 요구하지 않을 때만 허용됩니다.ApiProtectionResult예상되지 않습니다.API 키와 크기를 확인한 다음 속도 제한을 검사합니다.
ApiProtection::getRateLimit(string $clientId)클라이언트 식별자입니다.요청을 기록하지 않습니다.RateLimitResult예상되지 않습니다.속도 제한 헤더를 추가하는 데 사용하세요.
new ApiKeyValidator(array $validKeys = [])평문 유효 키 목록입니다.빈 목록은 모든 키를 거부합니다.ApiKeyValidator예상되지 않습니다.비밀 값은 코드 외부에 저장하고 구성을 통해 주입하세요.
ApiKeyValidator::validate(string $key)원시 키입니다.구성된 평문 키와 타이밍 안전 방식으로 비교합니다.bool예상되지 않습니다.민감한 매개변수입니다. 원시 키를 로그에 기록하지 마세요.
ApiKeyValidator::addKey(string $key)원시 키입니다.해시된 키를 새 검증기 인스턴스에 추가합니다.self예상되지 않습니다.반환된 인스턴스를 갱신된 검증기로 취급하세요.
ApiKeyValidator::revokeKey(string $key)원시 키입니다.일치하는 해시를 새 검증기 인스턴스에서 제거합니다.self예상되지 않습니다.반환된 인스턴스를 갱신된 검증기로 취급하세요.
ApiKeyValidator::hashKey(string $key)원시 키입니다.저장용 해시 표현을 생성합니다.string예상되지 않습니다.해시를 로그나 클라이언트 응답에 노출하지 마세요.
ApiKeyValidator::validateHashed(string $key, array $hashedKeys)원시 키와 후보 해시입니다.제공된 해시와의 상수 시간 비교입니다.bool예상되지 않습니다.사용자 정의 키 저장소를 위한 저수준 헬퍼입니다.
new ApiProtectionConfig(int $maxRequestsPerMinute = 60, int $maxRequestsPerHour = 1000, int $maxPayloadSizeBytes = 10485760, array $allowedOrigins = [], bool $requireApiKey = true, string $apiKeyHeader = 'X-Api-Key', int $rateLimitWindowSeconds = 60)요청 제한, 페이로드 제한, 허용된 오리진, API 키 요구 사항, 헤더 이름, 윈도 길이입니다.60/minute, 1000/hour, 10 MiB 페이로드, API 키 필요가 기본값입니다.ApiProtectionConfig예상되지 않습니다.테스트에서 직접 생성하거나 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입니다.키가 없으면 생성자 기본값을 사용합니다.ApiProtectionConfig예상되지 않습니다.프레임워크 구성 주입에 사용하세요.
ApiProtectionConfig::isValid()없음.양수 제한과 일관된 size/window 값이 필요합니다.bool예상되지 않습니다.엔드포인트를 노출하기 전에 검증하세요.
new ApiProtectionResult(bool $allowed, string $denialReason = '', ?RateLimitResult $rateLimit = null)결정, 거부 사유, 선택적 속도 제한 결과입니다.빈 거부 사유와 속도 제한 결과 없음입니다.ApiProtectionResult예상되지 않습니다.이 결과는 ApiProtection::checkRequest()가 반환합니다.
ApiProtectionResult::toHeaders()없음.속도 데이터가 있을 때 속도 제한 헤더를 내보냅니다.array<string, string>예상되지 않습니다.Worker 또는 프레임워크 응답에 첨부하세요.
new RateLimitResult(bool $allowed, int $remainingRequests, int $retryAfterSeconds, string $clientId)결정, 남은 횟수, 재시도 지연, 클라이언트 ID입니다.기본값이 없습니다.RateLimitResult예상되지 않습니다.단일 검사 결과를 나타내는 불변 결과입니다.
RateLimitResult::toHeaders()없음.남은 제한과 재설정 헤더를 내보냅니다.array<string, string>예상되지 않습니다.관측 가능성 및 클라이언트 백오프에 사용하세요.
new RateLimitEntry(string $clientId, int $requestCount = 0, int $windowStart = 0, int $hourlyCount = 0, int $hourlyWindowStart = 0)클라이언트 ID와 가변 카운터입니다.카운터는 0에서 시작합니다.RateLimitEntry예상되지 않습니다.인메모리 추적 객체입니다.
RateLimitEntry::increment()없음.단일 client/window에 대한 인메모리 카운터를 증가시킵니다.void예상되지 않습니다.내부적으로 ApiProtection가 사용하는 저수준 헬퍼입니다.
RateLimitEntry::isExpired(int $windowSeconds)윈도 길이(초)입니다.현재 시간과 비교합니다.bool예상되지 않습니다.런타임 만료 헬퍼입니다.
RateLimitEntry::isExpiredAt(int $now, int $windowSeconds)클럭 값과 윈도 길이입니다.제공된 클럭 값과 비교합니다.bool예상되지 않습니다.결정적 테스트 헬퍼입니다.
RateLimitEntry::reset()없음.횟수와 윈도 시작 시간을 재설정합니다.void예상되지 않습니다.새 윈도가 시작될 때 사용됩니다.

렌더링된 PDF를 Cloudflare R2에 저장할 때 이 표를 사용하세요. 아카이브 서비스와 해당 구성, 객체 키 타입, 그리고 URL을 노출하기 전에 검사할 업로드 결과를 다룹니다.

심볼매개변수기본 동작반환값발생시키거나 실패하는 조건참고
new R2ArchiveManager(R2ArchiveConfig $config, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory)R2 구성과 PSR HTTP factories/client입니다.생성 중에는 네트워크 호출이 없습니다.R2ArchiveManager컨테이너 연결 오류입니다.주 아카이브 서비스입니다.
R2ArchiveManager::upload(string $pdfData, string $filename, array $metadata = [])원시 PDF 바이트, 원본 파일 이름, 문자열 메타데이터입니다.빈 메타데이터, 날짜로 분할된 키입니다.R2UploadResult구성, 크기, HTTP 또는 전송 실패 시 실패 결과를 반환합니다.정상적인 업로드 실패에 대해서는 예외를 발생시키지 않습니다.
R2ArchiveManager::generateSignedUrl(string $key, int $expiresInSeconds = 3600)객체 키와 URL TTL입니다.1시간 동안 유효한 서명 URL입니다.string유효하지 않은 구성으로 인한 서명 오류입니다.민감한 PDF는 TTL을 짧게 유지하세요.
R2ArchiveManager::buildObjectKey(string $filename)원본 파일 이름입니다.구성된 경로 접두사와 현재 날짜를 사용합니다.R2ObjectKey예상되지 않습니다.예측 가능한 아카이브 분할에 사용하세요.
R2ArchiveManager::createPutRequest(R2ObjectKey $key, string $data, array $metadata = [])객체 키, 원시 바이트, 메타데이터입니다.PUT 요청에 서명합니다.RequestInterface요청 생성 오류입니다.사용자 정의 전송을 위한 저수준 API입니다.
new R2ArchiveConfig(string $bucketName, string $accountId, string $accessKeyId, string $secretAccessKey, string $endpoint = '', string $pathPrefix = 'pdfs/', int $maxFileSizeBytes = 104857600)버킷, 계정 ID, 자격 증명, 엔드포인트 재정의, 키 접두사, 최대 객체 크기입니다.엔드포인트를 파생하고, pdfs/ 접두사를 사용하며, 최대 객체 크기는 100 MiB입니다.R2ArchiveConfigInvalidArgumentException은 유효하지 않은 버킷 이름에 대해 발생합니다.자격 증명은 비밀을 담은 구성으로 취급하세요.
R2ArchiveConfig::fromArray(array $data)계정 ID, 버킷, 자격 증명, 경로 접두사, 엔드포인트 재정의, 최대 크기입니다.값이 없으면 생성자 기본값을 사용합니다.R2ArchiveConfig제공된 경우 유효하지 않은 버킷 이름입니다.애플리케이션 구성 주입에 사용하세요.
R2ArchiveConfig::isValid()없음.계정, 버킷, 액세스 키, 시크릿 키가 모두 비어 있지 않아야 합니다.bool예상되지 않습니다.유효하지 않은 구성은 업로드가 구조화된 결과와 함께 실패하게 만듭니다.
R2ArchiveConfig::getEndpoint()없음.명시적 엔드포인트를 사용하거나 계정 ID에서 Cloudflare R2 엔드포인트를 파생합니다.string예상되지 않습니다.서명된 요청 생성에 사용됩니다.
new R2ObjectKey(string $key, string $bucket)전체 객체 키와 버킷입니다.정규화가 없습니다.R2ObjectKey예상되지 않습니다.일반적으로 R2ObjectKey::generate()가 생성합니다.
R2ObjectKey::generate(string $prefix, string $filename, ?DateTimeInterface $date = null)접두사, 원본 파일 이름, 선택적 날짜입니다.날짜로 분할하고 정제한 객체 키입니다.R2ObjectKey예상되지 않습니다.테스트에서 날짜를 주입하면 결정적 키를 만들 수 있습니다.
R2ObjectKey::fullPath()없음.분할 경로와 객체 파일 이름을 결합합니다.string예상되지 않습니다.이 값을 객체 키로 저장하세요.
new R2UploadResult(bool $success, string $key, string $etag = '', int $size = 0, string $error = '')성공 플래그, 객체 키, ETag, 바이트 크기, 오류 메시지입니다.ETag는 비어 있고, 크기는 0이며, 오류는 비어 있습니다.R2UploadResult예상되지 않습니다.이 결과는 R2ArchiveManager::upload()가 반환합니다.
R2UploadResult::isValid()없음.업로드가 성공하고 키와 ETag가 모두 존재할 때 유효합니다.bool예상되지 않습니다.URL을 노출하기 전에 확인하세요.
R2UploadResult::publicUrl(string $customDomain = '')선택적 사용자 정의 공개 도메인입니다.사용자 정의 도메인이 제공되지 않으면 객체 키만 반환합니다.string예상되지 않습니다.정책이 허용하지 않는 한 민감한 문서에는 공개 URL을 피하세요.

저수준 연결에서만 이 표를 사용하세요. cURL 수준의 IP/SPKI 고정과, Worker에 연결할 수 없을 때 폴백 경로로 사용되는 로컬 렌더러 계약을 다룹니다.

심볼매개변수기본 동작반환값발생시키거나 실패하는 조건참고
new PinnedCurlTransport(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory, array $pinnedIps = [], array $pinnedPublicKeys = [], int $timeoutSeconds = 30)PSR-17 팩토리, 고정 IP, 고정 공개 키, 타임아웃입니다.고정은 없고, 타임아웃은 30초입니다.PinnedCurlTransport예상되지 않습니다.cURL 수준의 고정이 필요할 때만 사용하세요.
PinnedCurlTransport::sendRequest(RequestInterface $request)PSR-7 요청입니다.구성된 타임아웃과 고정 제어로 cURL을 통해 전송합니다.ResponseInterfacePSR-18 전송 예외입니다.프레임워크 HTTP 클라이언트가 동일한 고정 정책을 강제할 수 없을 때만 사용하세요.
PinnedCurlTransport::buildCurlOptions(RequestInterface $request, string $host, int $port)요청, 대상 호스트, 대상 포트입니다.내부적으로 sendRequest()가 사용하는 cURL 옵션 배열을 빌드합니다.array유효하지 않은 요청 또는 고정 구성 오류입니다.저수준 테스트 및 진단 후크입니다.
LocalRendererInterface::render(string $html, array $options = [])HTML과 렌더러 옵션입니다.계약만 정의하며, 구현이 기본값을 결정합니다.string구현별 렌더 오류입니다.Worker 렌더링을 사용할 수 없을 때 로컬 폴백으로 사용됩니다.
LocalRendererFactoryInterface::create()없음.로컬 렌더러 구현을 생성합니다.LocalRendererInterface팩토리 또는 의존성 오류입니다.폴백 렌더러 생성 책임을 CloudflareHtmlRenderer 외부에 둡니다.
  • Worker URL을 네트워크 경계로 취급하세요. 렌더 전에 대상, 크기, 인증을 검증하세요.
  • API 보호 결과를 예외 제어 흐름이 아니라 정책 출력으로 사용하세요.
  • R2 업로드는 구조화된 성공 또는 오류 결과를 반환합니다. 두 경로를 모두 처리하세요.