Pular para o conteúdo

Uso em produção: fallback, telemetria, arquivamento e proteção

Esta página cobre quatro preocupações de produção além da renderização básica: fallback local, telemetria de edge, arquivamento no Cloudflare R2 e a camada de proteção de application programming interface (API) de entrada. Cada seção corresponde a um comportamento de classe verificado.

Quando o Worker está inacessível e fallbackToLocal é true, a bridge delega a renderização a um renderizador local. Forneça esse renderizador por meio de LocalRendererFactoryInterface. A bridge o cria de forma lazy; por isso, o create() da factory é executado apenas no caminho de fallback.

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

Conecte a factory ao renderizador:

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

Quando o fallback é executado, o renderLocation do resultado é a string literal local, e heightPt é 0.0. O caminho local não informa uma localização de edge nem uma altura medida. A bridge passa a largura solicitada ao renderizador local por meio da chave de opção widthPt.

Lido diretamente de CloudflareHtmlRenderer:

SituaçãoResultado
Config incompleta, fallbackToLocal: falseCloudflareNotAvailableException
Config incompleta, fallbackToLocal: true, factory conectadaRenderização local
Worker lança um erro de transporte, fallback habilitado, factory conectadaRenderização local, registrada em warning e depois em info
Worker lança, fallback habilitado, Artisan instalado, sem factoryCloudflareNotAvailableException nomeando a factory ausente
Worker lança, fallback habilitado, Artisan não instaladoCloudflareNotAvailableException nomeando o pacote ausente
Worker retorna um erro Hypertext Transfer Protocol (HTTP) / corpo malformadoCloudflareRenderException, nunca faz fallback

A última linha é crítica. Um Worker que retorna um erro representa uma falha de renderização, não uma falha de acessibilidade. A bridge relança essa falha para que o código consiga distinguir uma renderização quebrada de um edge inacessível.

Toda renderização bem-sucedida no caminho binário inclui telemetria nos headers de resposta:

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

O renderizador lê renderLocation do header de resposta CF-Ray e usa o segmento após o último hífen. Para CF-Ray: 8abc123def456-TPE, a localização é TPE. Quando o header está ausente, a localização é uma string vazia. No caminho de resposta JavaScript Object Notation (JSON), o valor vem do campo JSON renderLocation. Trate esses valores como sinais de observabilidade do Worker, não como garantias da plataforma.

R2ArchiveManager faz upload de bytes Portable Document Format (PDF) para o Cloudflare R2 por meio da API compatível com Amazon Simple Storage Service (S3) e assina as requisições com 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]);
}

Comportamento verificado a partir de R2ArchiveManager e R2ObjectKey:

  • A chave do objeto é particionada por data no formato: <pathPrefix><Y>/<m>/<d>/<sanitized-filename>, por exemplo invoices/2026/05/18/invoice-2026-0042.pdf.
  • O nome do arquivo é sanitizado: basename() remove o path traversal e, em seguida, os null bytes e caracteres de controle (\x00\x1f, \x7f) são removidos. Um resultado vazio torna-se document.pdf.
  • Metadados personalizados são enviados como headers x-amz-meta-<lowercased-key> e incluídos no conjunto de signed-header da V4.
  • Arquivos maiores que maxFileSizeBytes (padrão 104857600) são rejeitados antes de qualquer requisição, retornando um R2UploadResult com success: false.
  • R2UploadResult::isValid() exige success, uma key não vazia e um etag não vazio.
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() constrói uma URL GET assinada via query com AWS Signature V4 e um valor X-Amz-Expires que você controla (padrão 3600 segundos). A requisição canônica usa o sentinela de content-hash UNSIGNED-PAYLOAD. Uma URL de leitura assinada via query usa esse formato porque o corpo não faz parte da requisição assinada. Isto descreve o comportamento de assinatura implementado pelo pacote, conforme lido de R2ArchiveManager. A documentação de serviço da Amazon define o AWS Signature Version 4, não um padrão de standards development organization (SDO); portanto, nenhuma cláusula normativa é fixada aqui. As chaves de acesso a objetos são #[SensitiveParameter]; mantenha-as fora dos logs.

R2UploadResult::publicUrl($customDomain) retorna a chave nua quando você não fornece um domínio, ou https://<domain>/<key> quando você fornece. Ela adiciona um esquema Hypertext Transfer Protocol Secure (HTTPS) quando o domínio informado não tem nenhum. Ela não torna público um bucket privado; isso continua sendo uma preocupação de configuração do bucket R2.

ApiProtection é a camada que você aplica às requisições de renderização que chegam a um gateway PHP na frente do Worker. Ela verifica em uma ordem fixa: API key, depois tamanho do payload, depois 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;
}

Comportamento verificado:

  • A ordem é API key → tamanho do payload → rate limit. A primeira verificação que falha interrompe o fluxo com um denialReason específico.
  • ApiKeyValidator::validate() usa hash_equals() para comparação resistente a timing e rejeita uma chave vazia. validateHashed() compara hashes Secure Hash Algorithm 256-bit (SHA-256) para armazenamento de chaves em repouso. Os parâmetros de chave carregam #[SensitiveParameter].
  • O store de rate-limit é em memória por processo. Ele rastreia uma janela por minuto (rateLimitWindowSeconds, padrão 60) e uma janela por hora (fixa em 3600 segundos). Ele não persiste entre workers nem entre reinicializações. Para compartilhar limites entre processos, coloque um store compartilhado à frente dele.
  • ApiProtectionResult::toHeaders() sempre adiciona X-Content-Type-Options: nosniff e X-Frame-Options: DENY, e mescla os headers de rate-limit (X-RateLimit-Remaining, X-RateLimit-Reset, mais Retry-After quando a requisição é negada).

Esta bridge não assina PDFs. Para montar um pipeline de assinatura em produção, renderize no edge e depois assine os bytes retornados com o engine:

  1. render()CloudflareRenderResult::$pdfData.
  2. Entregue $pdfData ao nextpdf/core (ou ao NextPDF Pro para assinatura PDF Advanced Electronic Signatures (PAdES) B-B). Os perfis de long-term-validation são um recurso do Enterprise; esta bridge core não declara suporte a nenhum dos dois recursos.

Mantenha a etapa de assinatura no próprio processo para que a chave de assinatura nunca cruze o limite do edge.

  • /integrations/cloudflare/security-and-operations/ — pinning, defesa contra server-side request forgery (SSRF), rotação de segredos e o runbook operacional.
  • /integrations/cloudflare/troubleshooting/ — catálogo de modos de falha.
  • /integrations/cloudflare/configuration/ — cada campo e padrão.