Renderize na edge com Cloudflare e fallback local
Em resumo
Seção intitulada “Em resumo”A ponte Cloudflare envia o HTML para um endpoint de renderização do Cloudflare Worker e retorna o PDF. A renderização roda na edge, então você não precisa operar um processo de navegador de longa duração. Você cria uma configuração somente HTTPS, conecta um cliente PHP Standards Recommendation (PSR)-18 e factories PSR-17, chama render() e pode adicionar um renderizador local para Workers inacessíveis. Este guia mostra a chamada de renderização, o caminho de fallback e os controles contra Server-Side Request Forgery (SSRF), rebinding de Domain Name System (DNS) e fixação de chave pública de Transport Layer Security (TLS) que a ponte aplica antes que qualquer requisição saia do processo.
Pré-requisitos, de antemão:
- O core do NextPDF e o
nextpdf/cloudflareestão instalados. - Um endpoint do Worker serve o contrato de renderização sobre HTTPS e aceita um token bearer. A ponte rejeita uma URL de Worker que não seja HTTPS antes de enviar qualquer coisa.
- Um cliente PSR-18 (por exemplo, Guzzle 7) e factories PSR-17 de requisição e stream estão disponíveis. Para o transporte cURL fixado, forneça também uma factory de resposta PSR-17 e a
ext-curl. - Para o fallback local, o
nextpdf/artisan(ou outro renderizador local) está disponível.
Este é um guia prático. Para a primeira renderização executável, comece pelo quickstart da Cloudflare.
Instalação
Seção intitulada “Instalação”Instale a ponte, um cliente PSR-18 e factories PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzlePara o fallback local, instale um renderizador local que a ponte possa chamar.
composer require nextpdf/artisanCarregue o token bearer do Worker e quaisquer credenciais do R2 a partir de variáveis de ambiente ou de um gerenciador de segredos. Nunca faça commit deles.
Visão conceitual
Seção intitulada “Visão conceitual”CloudflareHtmlRenderer::render() valida o HTML e o destino, envia um POST autenticado para o Worker e analisa a resposta. O Worker retorna bytes brutos de PDF (Content-Type: application/pdf) ou um corpo JSON com um campo pdf em base64. O renderizador mapeia a resposta para um final readonly CloudflareRenderResult que contém os bytes, a largura solicitada, a altura, a localização de renderização (derivada do cabeçalho CF-Ray) e o tempo de renderização.
A ponte separa as falhas em duas classes explícitas:
CloudflareRenderException— o Worker respondeu, mas a renderização falhou (um erro HTTP ou um corpo que não começa com%PDF). Esta é uma falha de renderização e nunca é repetida com fallback.CloudflareNotAvailableException— a edge não pôde ser alcançada e nenhum fallback utilizável estava disponível.
O fallback local cobre o segundo caso. Quando o Worker não pode ser alcançado e fallbackToLocal é true, a ponte chama a LocalRendererFactoryInterface que você fornece. Ela faz isso de forma preguiçosa: o create() da factory roda apenas no caminho de fallback. Em uma renderização de fallback, o renderLocation do resultado é a string literal local.
A ponte protege o limite de rede antes que qualquer requisição saia do PHP. Ela rejeita uma URL de Worker que não seja HTTPS. Ela rejeita um host de Worker que resolve para espaço de endereço privado ou reservado, verificando todos os registros A e AAAA em vez de apenas o primeiro. Ela também resolve o host novamente imediatamente antes de conectar, fechando a janela time-of-check/time-of-use (TOCTOU) contra rebinding de DNS. Quando você fornece uma factory de resposta PSR-17 e um conjunto de IPs resolvidos ou pins de Subject Public Key Info (SPKI), a ponte usa um transporte cURL fixado. Esse transporte vincula a conexão aos IPs verificados (CURLOPT_RESOLVE), aplica a fixação de chave pública de TLS (CURLOPT_PINNEDPUBLICKEY), verifica o peer e o host e não segue redirecionamentos.
Superfície de API
Seção intitulada “Superfície de API”// Configuration (final readonly):new CloudflareRendererConfig( string $workerUrl, // required, must be HTTPS string $apiToken, // required, #[SensitiveParameter] int $renderTimeout = 30, string $defaultCss = '', int $maxHtmlSize = 5_000_000, ?string $r2FontBucket = null, bool $fallbackToLocal = true, list<string> $pinnedPublicKeys = [], // sha256/<base64> list<string> $backupPublicKeys = [],)CloudflareRendererConfig::fromArray(array $config): self
// The renderer:new CloudflareHtmlRenderer( CloudflareRendererConfig $config, ClientInterface $httpClient, // PSR-18 RequestFactoryInterface $requestFactory, // PSR-17 StreamFactoryInterface $streamFactory, // PSR-17 ?LoggerInterface $logger = null, // PSR-3 ?LocalRendererFactoryInterface $localRendererFactory = null, ?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null, ?ResponseFactoryInterface $responseFactory = null, // enables pinned transport)CloudflareHtmlRenderer::render(string $html, float $widthPt = 595.28, float $heightPt = 0.0, list<string> $fontFiles = []): CloudflareRenderResultCloudflareHtmlRenderer::isAvailable(): boolrender() usa por padrão a largura A4 (595.28 pontos) e a altura detectada automaticamente (heightPt: 0). Para a referência completa de campos e o mapa de chaves do fromArray(), consulte a página de configuração da Cloudflare em Veja também.
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início rápido”Crie a configuração, construa o renderizador, renderize e grave os bytes.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;use GuzzleHttp\Psr7\HttpFactory;use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;use NextPDF\Cloudflare\Exception\CloudflareNotAvailableException;use NextPDF\Cloudflare\Exception\CloudflareRenderException;
$config = new CloudflareRendererConfig( workerUrl: 'https://pdf-renderer.example.workers.dev/render', apiToken: getenv('CF_PDF_TOKEN') ?: throw new RuntimeException('CF_PDF_TOKEN not set'),);
$httpFactory = new HttpFactory();
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: new Client(), requestFactory: $httpFactory, streamFactory: $httpFactory, responseFactory: $httpFactory, // enables the pinned cURL transport);
try { $result = $renderer->render('<h1>Hello from the edge</h1>');
if (!$result->isValid()) { throw new RuntimeException('Worker did not return a valid PDF'); }
file_put_contents('output.pdf', $result->pdfData);} catch (CloudflareRenderException $exception) { // Worker answered but the render failed. Not retried with fallback. fwrite(STDERR, 'Render failed: ' . $exception->getMessage() . PHP_EOL); exit(1);} catch (CloudflareNotAvailableException $exception) { // Edge unreachable and no usable fallback. fwrite(STDERR, 'Edge unavailable: ' . $exception->getMessage() . PHP_EOL); exit(2);}O token vem do ambiente e nunca é codificado de forma fixa. O workerUrl deve usar HTTPS; a ponte rejeita uma URL http:// antes de enviar qualquer requisição.
Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”Em produção, conecte uma factory de renderizador local para que um Worker inacessível use o fallback em vez de fazer a requisição falhar. Configure os pins de TLS com um pin de backup. O create() da factory roda apenas no caminho de fallback.
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;use NextPDF\Cloudflare\Contract\LocalRendererFactoryInterface;use NextPDF\Cloudflare\Contract\LocalRendererInterface;
final readonly class ArtisanLocalRendererFactory implements LocalRendererFactoryInterface{ public function __construct(private ChromeHtmlRenderer $chrome) {}
public function create(): LocalRendererInterface { return new readonly class($this->chrome) implements LocalRendererInterface { public function __construct(private ChromeHtmlRenderer $chrome) {}
/** @param array<string, mixed> $options */ public function render(string $html, array $options = []): string { $widthPt = (float) ($options['widthPt'] ?? 595.28); // A4 width $heightPt = (float) ($options['heightPt'] ?? 0.0); // 0 = auto-fit
return $this->chrome->render($html, $widthPt, $heightPt)->getPdfData(); } }; }}Conecte a factory e os pins ao renderizador.
<?php
declare(strict_types=1);
use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;
$config = CloudflareRendererConfig::fromArray([ 'worker_url' => getenv('CF_WORKER_URL') ?: '', 'api_token' => getenv('CF_PDF_TOKEN') ?: '', 'render_timeout' => 60, 'fallback_to_local' => true, 'pinned_public_keys' => ['sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg='], 'backup_public_keys' => ['sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys='],]);
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory, logger: $logger, localRendererFactory: new ArtisanLocalRendererFactory($chrome), responseFactory: $httpFactory,);Quando o fallback roda, o renderLocation do resultado é local e heightPt é 0.0. A ponte registra o fallback em warning e depois em info. Sempre configure um pin de backup antes da rotação de certificado, para que uma rotação planejada não bloqueie o acesso da ponte ao endpoint.
Casos extremos e armadilhas
Seção intitulada “Casos extremos e armadilhas”- Um erro do Worker não é uma falha de acessibilidade. Um Worker que retorna um erro HTTP ou um corpo malformado levanta
CloudflareRenderExceptione nunca é repetido com fallback. Apenas uma edge inacessível recorre ao fallback. Mantenha os dois braços de catch distintos. - O fallback precisa tanto da flag quanto de uma factory. Com
fallbackToLocal: true, mas sem uma factory conectada, um Worker inacessível levantaCloudflareNotAvailableExceptione nomeia a factory ausente. Conecte a factory. - O
isAvailable()é uma indicação, não uma garantia. Ele envia umHEADautenticado e retornatruepara um status abaixo de500; oPOSTseguinte ainda pode falhar. Não o trate como um contrato. - A fixação é opcional. Um conjunto vazio de pins desativa a fixação. Use um conjunto vazio apenas com uma cadeia de certificados estável e conhecida, e mantenha um pin de backup depois de fixar.
- O
fontFilesprecisa de um bucket R2. O argumentofontFilessó importa quando a configuração definer2FontBucket; caso contrário, não tem efeito. - A ponte não assina. Ela retorna bytes de PDF. Renderize na edge e, em seguida, assine no seu próprio processo, para que a chave de assinatura nunca atravesse o limite da edge.
Desempenho
Seção intitulada “Desempenho”A renderização na edge remove dos seus hosts o custo do navegador. Você ainda paga por uma ida e volta HTTPS até o Worker, além do tempo de renderização do Worker, que o resultado informa como renderTimeMs. A ponte aplica o timeout configurado por meio do transporte fixado. Defina-o com base na latência medida do Worker, com folga, e mantenha-o abaixo de qualquer timeout de gateway upstream. O pacote declara apenas os limites que ele próprio aplica. Ele não faz nenhuma afirmação sobre os tetos de CPU, memória ou corpo de requisição da plataforma Cloudflare. Para esses limites, consulte a documentação da Cloudflare e o seu Worker.
Notas de segurança
Seção intitulada “Notas de segurança”- O destino é validado antes que a requisição saia do PHP. URLs que não sejam HTTPS são rejeitadas. Um host que resolve para espaço de endereço privado ou reservado é rejeitado em todos os registros A e AAAA. O host é resolvido novamente imediatamente antes de conectar, como defesa contra rebinding de DNS.
- O transporte fixado vincula DNS e TLS. Com uma factory de resposta e pins configurados, a ponte vincula a conexão aos IPs verificados, aplica a fixação de SPKI, verifica o peer e o host e se recusa a seguir redirecionamentos para um host não verificado.
- A entrada é limitada. HTML acima de
maxHtmlSize(padrão 5 MB), um data URI base64 superdimensionado e qualquer tag<meta http-equiv="refresh">são rejeitados antes de a requisição ser enviada. - Os segredos são ocultados e imutáveis. O
apiTokene as chaves do R2 carregam#[SensitiveParameter], de modo que os rastreamentos de pilha os ocultam, e os objetos de configuração sãofinal readonly. Carregue os segredos do ambiente ou de um gerenciador de segredos; nunca faça commit deles. - Nunca escreva um bloco
catchvazio. Cada exemplo captura o tipo de exceção específico e registra ou sai com um código definido.
O modelo de segurança completo está na página de segurança e operações da Cloudflare em Veja também. Ela cobre a defesa contra SSRF e rebinding de DNS, as operações de fixação, o manuseio de segredos e as cláusulas relevantes do OWASP e da RFC 7469.
Conformidade
Seção intitulada “Conformidade”Este guia não faz nenhuma afirmação normativa de padrões por conta própria. Nas páginas upstream de segurança e operações e de configuração da Cloudflare, a resolução de DNS de todos os registros e a reverificação TOCTOU da ponte correspondem às orientações de prevenção de SSRF do OWASP, e a fixação de chave pública de TLS e a recuperação por pin de backup correspondem à RFC 7469. Esta página do cookbook reafirma o uso e remete essas citações a essas páginas. A ponte não realiza assinatura e não faz nenhuma afirmação de conformidade de assinatura.
Veja também
Seção intitulada “Veja também”- Renderize HTML em PDF com o renderizador Chrome do Artisan — o renderizador em processo usado como fallback local aqui.
- Quickstart da Cloudflare — a primeira renderização na edge e o modelo de resultado.
- Segurança e operações da Cloudflare — SSRF, rebinding de DNS, fixação e rotação de segredos.
- Uso em produção da Cloudflare — conexão de fallback, telemetria, arquivamento no R2 e proteção de API.