Przejdź do głównej zawartości

Użycie produkcyjne — rezerwa lokalna, telemetria, archiwizacja i ochrona

Strona omawia cztery zagadnienia produkcyjne wykraczające poza podstawowe renderowanie: rezerwę lokalną, telemetrię z brzegu sieci, archiwizację w Cloudflare R2 oraz warstwę ochrony przychodzącego interfejsu programowania aplikacji (API). Każda sekcja odwołuje się do zweryfikowanego zachowania klas.

Jeśli Worker jest nieosiągalny, a fallbackToLocal ma wartość true, mostek deleguje renderowanie do lokalnego mechanizmu renderującego. Udostępnij lokalny mechanizm renderujący przez LocalRendererFactoryInterface. Mostek tworzy go leniwie, więc metoda create() fabryki jest wywoływana tylko na ścieżce rezerwowej.

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

Podłącz fabrykę do mechanizmu renderującego:

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

Gdy uruchamia się ścieżka rezerwowa, renderLocation w wyniku jest dosłownym ciągiem local, a heightPt wynosi 0.0. Ścieżka lokalna nie raportuje lokalizacji brzegu sieci ani zmierzonej wysokości. Mostek przekazuje żądaną szerokość do lokalnego mechanizmu renderującego za pomocą klucza opcji widthPt.

Odczytane bezpośrednio z CloudflareHtmlRenderer:

SytuacjaWynik
Niekompletna konfiguracja, fallbackToLocal: falseCloudflareNotAvailableException
Niekompletna konfiguracja, fallbackToLocal: true, fabryka podłączonaRenderowanie lokalne
Worker zgłasza błąd transportu, rezerwa włączona, fabryka podłączonaRenderowanie lokalne, logowane na poziomie warning, a następnie info
Worker zgłasza błąd, rezerwa włączona, Artisan zainstalowany, brak fabrykiCloudflareNotAvailableException wskazujący brakującą fabrykę
Worker zgłasza błąd, rezerwa włączona, Artisan nie jest zainstalowanyCloudflareNotAvailableException wskazujący brakujący pakiet
Worker zwraca błąd protokołu Hypertext Transfer Protocol (HTTP) / zniekształconą treśćCloudflareRenderException, nigdy nie przechodzi do rezerwy

Ostatni wiersz jest kluczowy. Worker zwracający błąd oznacza niepowodzenie renderowania, a nie problem z osiągalnością. Mostek zgłasza ten błąd ponownie, aby kod mógł odróżnić nieudane renderowanie od nieosiągalnego brzegu sieci.

Każde udane renderowanie na ścieżce binarnej zawiera telemetrię z nagłówków odpowiedzi:

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

Mechanizm renderujący odczytuje renderLocation z nagłówka odpowiedzi CF-Ray i pobiera segment po ostatnim łączniku. Dla CF-Ray: 8abc123def456-TPE lokalizacją jest TPE. Jeśli nagłówka nie ma, lokalizacją jest pusty ciąg. Na ścieżce odpowiedzi JavaScript Object Notation (JSON) wartość pochodzi natomiast z pola JSON renderLocation. Traktuj te wartości jako sygnały obserwowalności pochodzące z Workera, a nie jako gwarancje platformy.

R2ArchiveManager przesyła bajty w formacie Portable Document Format (PDF) do Cloudflare R2 przez API zgodne z Amazon Simple Storage Service (S3) i podpisuje żądania za pomocą 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]);
}

Zachowanie zweryfikowane na podstawie R2ArchiveManager i R2ObjectKey:

  • Klucz obiektu jest partycjonowany według daty, w postaci: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, na przykład invoices/2026/05/18/invoice-2026-0042.pdf.
  • Nazwa pliku jest oczyszczana: basename() usuwa elementy przechodzenia po ścieżce, a następnie usuwane są bajty zerowe i znaki sterujące (\x00\x1f, \x7f). Pusty wynik staje się document.pdf.
  • Niestandardowe metadane są wysyłane jako nagłówki x-amz-meta-<lowercased-key> i wchodzą w skład zestawu podpisanych nagłówków V4.
  • Pliki większe niż maxFileSizeBytes (domyślnie 104857600) są odrzucane przed wysłaniem jakiegokolwiek żądania; metoda zwraca R2UploadResult z success: false.
  • R2UploadResult::isValid() wymaga success, niepustego key oraz niepustego etag.
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() buduje adres URL GET podpisany w parametrach zapytania z użyciem AWS Signature V4 oraz kontrolowaną przez ciebie wartością X-Amz-Expires (domyślnie 3600 sekund). Żądanie kanoniczne używa znacznika skrótu treści UNSIGNED-PAYLOAD. Adres URL do odczytu podpisany w parametrach zapytania używa tej postaci, ponieważ treść nie jest częścią podpisanego żądania. Opis ten odpowiada zachowaniu podpisywania zaimplementowanemu w pakiecie i odczytanemu z R2ArchiveManager. AWS Signature Version 4 definiuje dokumentacja usługowa Amazona, a nie norma organizacji opracowującej standardy (SDO), dlatego nie przypisano tutaj żadnej klauzuli normatywnej. Klucze dostępu do obiektów są oznaczone jako #[SensitiveParameter]; nie zapisuj ich w dziennikach.

R2UploadResult::publicUrl($customDomain) zwraca sam klucz, jeśli nie podasz domeny, albo https://<domain>/<key>, jeśli ją podasz. Dodaje schemat Hypertext Transfer Protocol Secure (HTTPS), jeśli podana domena go nie zawiera. Nie sprawia, że prywatny zasobnik staje się publiczny; pozostaje to kwestią konfiguracji zasobnika R2.

ApiProtection to warstwa stosowana do żądań renderowania, które trafiają do bramy PHP przed Workerem. Kontrole wykonuje w stałej kolejności: klucz API, następnie rozmiar ładunku, następnie limit szybkości.

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

Zweryfikowane zachowanie:

  • Kolejność to klucz API → rozmiar ładunku → limit szybkości. Pierwsza nieudana kontrola powoduje natychmiastowe przerwanie z określonym denialReason.
  • ApiKeyValidator::validate() używa hash_equals() do porównania odpornego na ataki czasowe i odrzuca pusty klucz. validateHashed() porównuje klucze ze skrótami Secure Hash Algorithm 256-bit (SHA-256) na potrzeby przechowywania kluczy w stanie spoczynku. Parametry kluczy są oznaczone atrybutem #[SensitiveParameter].
  • Magazyn limitu szybkości jest utrzymywany w pamięci oddzielnie dla każdego procesu. Śledzi okno minutowe (rateLimitWindowSeconds, domyślnie 60) oraz okno godzinowe (stałe 3600 sekund). Nie jest utrwalany między procesami roboczymi ani po ponownym uruchomieniu. Aby współdzielić limity między procesami, umieść przed nim współdzielony magazyn.
  • ApiProtectionResult::toHeaders() zawsze dodaje X-Content-Type-Options: nosniff oraz X-Frame-Options: DENY i łączy je z nagłówkami limitu szybkości (X-RateLimit-Remaining, X-RateLimit-Reset oraz Retry-After w przypadku odmowy).

Ten mostek nie podpisuje plików PDF. Aby zbudować produkcyjny potok podpisywania, renderuj na brzegu sieci, a następnie podpisz zwrócone bajty przy użyciu silnika:

  1. render()CloudflareRenderResult::$pdfData.
  2. Przekaż $pdfData do nextpdf/core (lub NextPDF Pro w celu podpisywania PDF Advanced Electronic Signatures (PAdES) B-B). Profile długoterminowej weryfikacji są funkcją Enterprise; ten mostek core nie udostępnia żadnej z tych funkcji.

Zachowaj krok podpisywania we własnym procesie, aby klucz podpisujący nigdy nie przekraczał granicy brzegu sieci.

  • /integrations/cloudflare/security-and-operations/ — przypinanie, ochrona przed fałszowaniem żądań po stronie serwera (SSRF), rotacja sekretów oraz operacyjny podręcznik procedur.
  • /integrations/cloudflare/troubleshooting/ — katalog trybów awarii.
  • /integrations/cloudflare/configuration/ — każde pole i wartość domyślna.