Zum Inhalt springen

Produktiver Einsatz — Fallback, Telemetrie, Archivierung, Schutz

Diese Seite behandelt vier Aspekte für den Produktivbetrieb, die das Paket über reines Rendering hinaus abdeckt: lokalen Fallback, Edge-Telemetrie, R2-Archivierung und die eingehende API-Schutzschicht. Jeder Abschnitt beschreibt verifiziertes Klassenverhalten.

Ist der Worker nicht erreichbar und steht fallbackToLocal auf true, delegiert die Bridge an einen lokalen Renderer. Stellen Sie diesen lokalen Renderer über LocalRendererFactoryInterface bereit. Die Bridge ruft die Factory erst bei Bedarf auf, sodass create() nur auf dem Fallback-Pfad ausgeführt wird.

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

Binden Sie die Factory in den Renderer ein:

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

Bei ausgeführtem Fallback ist renderLocation im Ergebnis die wörtliche Zeichenkette local, und heightPt ist 0.0. Der lokale Pfad meldet weder einen Edge-Standort noch eine gemessene Höhe. Die Bridge übergibt die angeforderte Breite über den Optionsschlüssel widthPt an den lokalen Renderer.

Direkt aus CloudflareHtmlRenderer abgeleitet:

SituationErgebnis
Konfiguration unvollständig, fallbackToLocal: falseCloudflareNotAvailableException
Konfiguration unvollständig, fallbackToLocal: true, Factory eingebundenLokales Rendering
Worker wirft einen Transportfehler, Fallback aktiviert, Factory eingebundenLokales Rendering, protokolliert auf warning, dann info
Worker wirft, Fallback aktiviert, Artisan installiert, keine FactoryCloudflareNotAvailableException, nennt die fehlende Factory
Worker wirft, Fallback aktiviert, Artisan nicht installiertCloudflareNotAvailableException, nennt das fehlende Paket
Worker liefert einen HTTP-Fehler / fehlerhaften Body zurückCloudflareRenderException, fällt niemals zurück

Die letzte Zeile enthält die entscheidende Unterscheidung. Antwortet ein Worker mit einem Fehler, handelt es sich um einen Render-Fehler, nicht um einen Erreichbarkeitsfehler. Der Fehler wird erneut geworfen, damit Ihr Code ein fehlerhaftes Rendering von einer nicht erreichbaren Edge unterscheiden kann.

Jedes erfolgreiche Rendering über den Binärpfad enthält Telemetrie, die aus den Antwortheadern abgeleitet wird:

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

Der Renderer liest renderLocation aus dem Antwortheader CF-Ray und verwendet das Segment nach dem letzten Bindestrich. Bei CF-Ray: 8abc123def456-TPE ist der Standort TPE. Wenn der Header fehlt, ist der Standort eine leere Zeichenkette. Auf dem JSON-Antwortpfad stammt der Wert stattdessen aus dem JSON-Feld renderLocation. Behandeln Sie diese Werte als Observability-Signale des Workers, nicht als Plattformgarantien.

R2ArchiveManager lädt PDF-Bytes über die S3-kompatible API in Cloudflare R2 hoch und signiert die Anfragen mit 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]);
}

Aus R2ArchiveManager und R2ObjectKey verifiziertes Verhalten:

  • Der Objektschlüssel ist nach Datum partitioniert: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, zum Beispiel invoices/2026/05/18/invoice-2026-0042.pdf.
  • Der Dateiname wird bereinigt: basename() wird angewendet, um Path-Traversal zu entfernen; anschließend werden Nullbytes und Steuerzeichen (\x00\x1f, \x7f) entfernt. Ein leeres Ergebnis wird zu document.pdf.
  • Benutzerdefinierte Metadaten werden als x-amz-meta-<lowercased-key>-Header gesendet und in die V4-signierte Headermenge aufgenommen.
  • Ein Upload, der größer als maxFileSizeBytes ist (Standard 104857600), wird vor dem Senden einer Anfrage abgelehnt und liefert ein R2UploadResult mit success: false zurück.
  • R2UploadResult::isValid() erfordert success, einen nicht leeren key und einen nicht leeren etag.
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() erstellt eine per Query signierte GET-URL nach AWS Signature V4 mit einem von Ihnen steuerbaren X-Amz-Expires (Standard 3600 Sekunden). Die kanonische Anfrage verwendet das Content-Hash-Sentinel UNSIGNED-PAYLOAD. Eine per Query signierte Lese-URL verwendet diese Form, weil der Body nicht Teil der signierten Anfrage ist. Dies beschreibt das im Paket implementierte Signierverhalten, wie aus R2ArchiveManager ausgelesen. AWS Signature Version 4 wird in Amazons Service-Dokumentation definiert, nicht in einem SDO-Standard; daher wird hier keine normative Klausel verankert. Objektzugriffsschlüssel sind mit #[SensitiveParameter] gekennzeichnet; halten Sie sie aus den Logs heraus.

R2UploadResult::publicUrl($customDomain) liefert den reinen Schlüssel zurück, wenn keine Domain angegeben ist, andernfalls https://<domain>/<key>. Es erzwingt ein HTTPS-Schema, wenn die angegebene Domain keines hat. Dadurch wird ein privater Bucket nicht öffentlich. Das ist Sache der R2-Bucket-Konfiguration.

ApiProtection ist die Schicht, die Sie auf Render-Anfragen anwenden, die bei einem dem Worker vorgeschalteten PHP-Gateway eintreffen. Sie führt drei Prüfungen in fester Reihenfolge aus: API-Schlüssel, dann Payload-Größe, dann Ratenbegrenzung.

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

Verifiziertes Verhalten:

  • Die Reihenfolge ist API-Schlüssel → Payload-Größe → Ratenbegrenzung. Die erste fehlgeschlagene Prüfung beendet die Verarbeitung mit einem spezifischen denialReason.
  • ApiKeyValidator::validate() verwendet hash_equals() für einen timingsicheren Vergleich und weist einen leeren Schlüssel zurück. validateHashed() vergleicht für die Schlüsselspeicherung im Ruhezustand gegen SHA-256-Hashes. Schlüsselparameter tragen #[SensitiveParameter].
  • Der Ratenbegrenzungsspeicher ist im Arbeitsspeicher pro Prozess. Er verfolgt ein Minutenfenster (rateLimitWindowSeconds, Standard 60) und ein Stundenfenster (fest 3600 Sekunden). Er bleibt nicht über Worker oder Neustarts hinweg erhalten. Für eine gemeinsame Begrenzung über mehrere Prozesse hinweg schalten Sie ihm einen gemeinsamen Speicher vor.
  • ApiProtectionResult::toHeaders() fügt stets X-Content-Type-Options: nosniff und X-Frame-Options: DENY hinzu und führt die Ratenbegrenzungsheader zusammen (X-RateLimit-Remaining, X-RateLimit-Reset, zuzüglich Retry-After bei Ablehnung).

Diese Bridge signiert keine PDFs. Eine produktive Signier-Pipeline rendert an der Edge und signiert anschließend die zurückgegebenen Bytes mit der Engine:

  1. render()CloudflareRenderResult::$pdfData.
  2. Übergeben Sie $pdfData an nextpdf/core (oder an NextPDF Pro für PAdES B-B-Signierung). Long-Term-Validation-Profile sind eine Enterprise-Fähigkeit; diese Core-Bridge beansprucht weder PAdES B-B-Signierung noch Long-Term-Validation-Profile.

Belassen Sie den Signierschritt in Ihrem eigenen Prozess, damit der Signaturschlüssel niemals die Edge-Grenze überschreitet.

  • /integrations/cloudflare/security-and-operations/ — Pinning, SSRF-Abwehr, Secret-Rotation, operatives Runbook.
  • /integrations/cloudflare/troubleshooting/ — Katalog der Fehlermodi.
  • /integrations/cloudflare/configuration/ — jedes Feld und jeder Standard.