Ga naar inhoud

Productiegebruik: fallback, telemetrie, archivering en bescherming

Deze pagina behandelt vier productieaspecten die verder gaan dan een eenvoudige render: lokale fallback, edge-telemetrie, archivering naar Cloudflare R2 en de beschermingslaag voor inkomende application programming interface (API)-verzoeken. Elke sectie sluit aan op gedrag dat in de klassen is geverifieerd.

Wanneer de Worker onbereikbaar is en fallbackToLocal op true staat, delegeert de bridge het renderen aan een lokale renderer. Lever die renderer aan via LocalRendererFactoryInterface. De bridge initialiseert die lazy, zodat create() van de factory alleen op het fallback-pad wordt uitgevoerd.

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

Koppel de factory aan de renderer:

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

Wanneer fallback wordt uitgevoerd, is renderLocation van het resultaat de letterlijke tekenreeks local en is heightPt gelijk aan 0.0. Het lokale pad rapporteert geen edge-locatie of gemeten hoogte. De bridge geeft de gevraagde breedte via de optiesleutel widthPt door aan de lokale renderer.

Rechtstreeks uitgelezen uit CloudflareHtmlRenderer:

SituatieUitkomst
Configuratie onvolledig, fallbackToLocal: falseCloudflareNotAvailableException
Configuratie onvolledig, fallbackToLocal: true, factory gekoppeldLokale render
Worker werpt een transportfout, fallback ingeschakeld, factory gekoppeldLokale render, gelogd op warning en vervolgens info
Worker werpt een fout, fallback ingeschakeld, Artisan geïnstalleerd, geen factoryCloudflareNotAvailableException die de ontbrekende factory benoemt
Worker werpt een fout, fallback ingeschakeld, Artisan niet geïnstalleerdCloudflareNotAvailableException die het ontbrekende pakket benoemt
Worker retourneert een Hypertext Transfer Protocol (HTTP)-fout of een misvormde bodyCloudflareRenderException, valt nooit terug

De laatste rij is cruciaal. Een Worker die een fout retourneert, is een renderfout, geen bereikbaarheidsfout. De bridge geeft die opnieuw door, zodat je code een mislukte render kan onderscheiden van een onbereikbare edge.

Elke geslaagde render via het binaire pad bevat telemetrie uit de responseheaders:

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

De renderer leest renderLocation uit de CF-Ray-responseheader en neemt het deel na het laatste koppelteken. Voor CF-Ray: 8abc123def456-TPE is de locatie TPE. Wanneer de header ontbreekt, is de locatie een lege tekenreeks. Op het JavaScript Object Notation (JSON)-responsepad komt de waarde in plaats daarvan uit het JSON-veld renderLocation. Beschouw deze waarden als observability-signalen van de Worker, niet als platformgaranties.

R2ArchiveManager uploadt Portable Document Format (PDF)-bytes naar Cloudflare R2 via de API die compatibel is met Amazon Simple Storage Service (S3) en ondertekent verzoeken met 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]);
}

Gedrag geverifieerd op basis van R2ArchiveManager en R2ObjectKey:

  • De objectsleutel wordt per datum gepartitioneerd als: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, bijvoorbeeld invoices/2026/05/18/invoice-2026-0042.pdf.
  • De bestandsnaam wordt opgeschoond: basename() verwijdert path traversal; daarna worden nullbytes en stuurtekens (\x00\x1f, \x7f) verwijderd. Een leeg resultaat wordt document.pdf.
  • Aangepaste metadata wordt verzonden als x-amz-meta-<lowercased-key>-headers, opgenomen in de set V4-ondertekende headers.
  • Bestanden die groter zijn dan maxFileSizeBytes (standaard 104857600) worden vóór elk verzoek afgewezen en retourneren een R2UploadResult met success: false.
  • R2UploadResult::isValid() vereist success, een niet-lege key en een niet-lege etag.
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() bouwt een GET-URL die met een AWS Signature V4-query is ondertekend, met een X-Amz-Expires-waarde die je zelf bepaalt (standaard 3600 seconden). Het canonieke verzoek gebruikt de UNSIGNED-PAYLOAD-content-hash-sentinel. Een query-ondertekende lees-URL gebruikt deze vorm omdat de body geen onderdeel is van het ondertekende verzoek. Dit beschrijft het geïmplementeerde ondertekeningsgedrag van het pakket, zoals uitgelezen uit R2ArchiveManager. De servicedocumentatie van Amazon definieert AWS Signature Version 4, niet een standaard van een standards development organization (SDO), dus hier wordt geen normatieve clausule vastgelegd. Object-toegangssleutels zijn #[SensitiveParameter]; houd ze uit de logs.

R2UploadResult::publicUrl($customDomain) retourneert alleen de sleutel wanneer je geen domein opgeeft, of https://<domain>/<key> wanneer je dat wel doet. De methode voegt een Hypertext Transfer Protocol Secure (HTTPS)-schema toe wanneer het opgegeven domein er geen heeft. Dit maakt een privé-bucket niet openbaar; dat blijft een kwestie van de R2-bucketconfiguratie.

ApiProtection is de laag die je toepast op render-verzoeken die binnenkomen bij een PHP-gateway vóór de Worker. Deze controleert in een vaste volgorde: API-sleutel, dan payloadgrootte, dan 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;
}

Geverifieerd gedrag:

  • De volgorde is API-sleutel → payloadgrootte → rate limit. De eerste mislukte controle breekt direct af met een specifieke denialReason.
  • ApiKeyValidator::validate() gebruikt hash_equals() voor een timingveilige vergelijking en wijst een lege sleutel af. validateHashed() vergelijkt met Secure Hash Algorithm 256-bit (SHA-256)-hashes voor at-rest opslag van sleutels. Sleutelparameters dragen #[SensitiveParameter].
  • De rate-limit-opslag is in-memory per proces. Deze houdt een venster per minuut bij (rateLimitWindowSeconds, standaard 60) en een venster per uur (vast op 3600 seconden). De inhoud blijft niet behouden over workers of herstarts heen. Om limieten over processen heen te delen, plaats je er een gedeelde opslag voor.
  • ApiProtectionResult::toHeaders() voegt altijd X-Content-Type-Options: nosniff en X-Frame-Options: DENY toe en neemt de rate-limit-headers op (X-RateLimit-Remaining, X-RateLimit-Reset, plus Retry-After wanneer geweigerd).

Deze bridge ondertekent geen PDF’s. Om een productie-ondertekeningspijplijn te bouwen, render je aan de edge en onderteken je vervolgens de geretourneerde bytes met de engine:

  1. render()CloudflareRenderResult::$pdfData.
  2. Geef $pdfData door aan nextpdf/core (of NextPDF Pro voor ondertekening met PDF Advanced Electronic Signatures (PAdES) B-B). Long-term-validation-profielen zijn een Enterprise-functie; deze core-bridge claimt geen van deze functies.

Houd de ondertekeningsstap in je eigen proces, zodat de ondertekeningssleutel nooit de edge-grens passeert.

  • /integrations/cloudflare/security-and-operations/ — pinning, verdediging tegen server-side request forgery (SSRF), rotatie van geheimen en het operationele runbook.
  • /integrations/cloudflare/troubleshooting/ — catalogus van foutmodi.
  • /integrations/cloudflare/configuration/ — elk veld en elke standaardwaarde.