コンテンツにスキップ

ローカルフォールバック付きの Cloudflare エッジレンダリング

Cloudflare ブリッジは、HTML を Cloudflare Worker レンダリングエンドポイントに送信して PDF を返します。レンダリングはエッジで実行されるため、長時間稼働するブラウザープロセスを運用する必要はありません。HTTPS のみを使う設定を構築し、PSR-18 クライアントと PSR-17 ファクトリを接続して render() を呼び出します。Worker に到達できない場合にブリッジが使用するローカルレンダラーも、任意で接続できます。このガイドでは、レンダリング呼び出し、フォールバックの判定、そしてリクエストがプロセス外へ出る前にブリッジが強制する SSRF、DNS リバインディング、TLS 公開鍵ピン留めの制御について説明します。

最初に前提条件を確認します。

  • NextPDF コアと nextpdf/cloudflare がインストールされていること。
  • Worker エンドポイントが HTTPS 上でレンダリングコントラクトを提供し、ベアラートークンを受け付けること。ブリッジは、何も送信する前に HTTPS 以外の Worker URL を拒否します。
  • PSR-18 クライアント(たとえば Guzzle 7)と PSR-17 のリクエストファクトリおよびストリームファクトリを利用できること。ピン留めされた cURL トランスポートを使う場合は、さらに PSR-17 レスポンスファクトリと ext-curl も用意してください。
  • ローカルフォールバックには、nextpdf/artisan(または別のローカルレンダラー)が利用できること。

これはハウツーガイドです。最初に動かせるレンダリング手順については、Cloudflare クイックスタートを参照してください。

ブリッジ、PSR-18 クライアント、PSR-17 ファクトリをインストールします。

Terminal window
composer require nextpdf/cloudflare guzzlehttp/guzzle

ローカルフォールバックには、ブリッジが委譲できるローカルレンダラーをインストールします。

Terminal window
composer require nextpdf/artisan

Worker のベアラートークンと R2 の認証情報は、環境変数またはシークレットマネージャーから取得します。決してコミットしないでください。

CloudflareHtmlRenderer::render() は HTML と宛先を検証し、認証付きの POST を Worker に送信してレスポンスを解析します。Worker は、生の PDF バイト列(Content-Type: application/pdf)または base64 の pdf フィールドを持つ JSON ボディのいずれかを返します。レンダラーは、その結果を final readonly CloudflareRenderResult にマッピングします。結果には、バイト列、要求された幅と高さ、レンダリング場所(CF-Ray ヘッダーから導出)、レンダリング時間が含まれます。

ブリッジは、2 つの障害クラスを意図的に分けています。

  • CloudflareRenderException — Worker は応答したものの、レンダリングが失敗した場合(HTTP エラー、または %PDF で始まらないボディ)。これはレンダリング失敗であり、フォールバックで再試行されることは決してありません
  • CloudflareNotAvailableException — エッジに到達できず、利用可能なフォールバックもなかった場合。

ローカルフォールバックは、2 つ目のケースを補います。Worker に到達できず、かつ fallbackToLocaltrue の場合、ブリッジは提供された LocalRendererFactoryInterface を呼び出します。これは遅延実行されるため、ファクトリの create() はフォールバックパス上でのみ実行されます。フォールバックでレンダリングした場合、結果の renderLocation はリテラル文字列 local になります。

ブリッジは、リクエストが PHP を離れる前にネットワーク境界を防御します。HTTPS 以外の Worker URL は拒否します。プライベートまたは予約済みのアドレス空間に解決される Worker ホストも拒否します。その際、最初のレコードだけでなく、A レコードと AAAA レコードをすべて検査します。さらに、接続の直前にホストを再解決し、DNS リバインディングに対する time-of-check/time-of-use のウィンドウを閉じます。PSR-17 レスポンスファクトリに加え、解決済み IP セットまたは SPKI ピンのいずれかを指定すると、ブリッジはピン留めされた cURL トランスポートを使用します。このトランスポートは、接続を検証済みの IP(CURLOPT_RESOLVE)に固定し、TLS 公開鍵ピン留め(CURLOPT_PINNEDPUBLICKEY)を強制し、ピアとホストを検証し、リダイレクトには従いません。

// 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 = []): CloudflareRenderResult
CloudflareHtmlRenderer::isAvailable(): bool

render() は、既定で A4 の幅(595.28 ポイント)と自動検出される高さ(heightPt: 0)を使用します。すべてのフィールドのリファレンスと fromArray() のキーマップについては、「関連項目」にリンクされている Cloudflare 設定ページを参照してください。

設定を組み立て、レンダラーを作成してレンダリングし、バイト列を書き込みます。

edge-quickstart.php
<?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);
}

トークンは環境から読み取られ、ハードコードされません。workerUrl は HTTPS でなければなりません。ブリッジは、リクエストを送信する前に http:// URL を拒否します。

本番環境では、Worker に到達できない場合にリクエストを失敗させるのではなくフォールバックできるよう、ローカルレンダラーファクトリを接続し、バックアップピンを含む TLS ピンを設定します。ファクトリの create() は、フォールバックパス上でのみ実行されます。

ArtisanLocalRendererFactory.php
<?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();
}
};
}
}

ファクトリとピンをレンダラーに接続します。

build the production renderer
<?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,
);

フォールバックが実行されると、結果の renderLocationlocalheightPt0.0 になります。ブリッジは、フォールバックを warning に続いて info でログに記録します。計画的なローテーションによってブリッジがエンドポイントから締め出されないよう、証明書をローテーションする前には必ずバックアップピンを設定してください。

  • Worker のエラーは到達不能とは別です。 HTTP エラーまたは不正な形式のボディで応答する Worker は CloudflareRenderException を発生させ、フォールバックで再試行されることはありません。フォールバックするのは、エッジに到達できない場合のみです。2 つの catch アームは区別して扱ってください。
  • フォールバックには、フラグとファクトリの両方が必要です。 fallbackToLocal: true でもファクトリが接続されていない場合、到達できない Worker は、欠落しているファクトリを示す CloudflareNotAvailableException を発生させます。 ファクトリを接続してください。
  • フォールバックには、フラグとファクトリの両方が必要です。 fallbackToLocal: true でもファクトリが接続されていない場合、Worker に到達できないと、欠落しているファクトリを示す CloudflareNotAvailableException が発生します。ファクトリを接続してください。
  • isAvailable() はヒントであり、保証ではありません。 これは認証付きの HEAD を送信し、ステータスが 500 未満の場合にのみ true を返します。続く POST は、それでも失敗する可能性があります。これをコントラクトとして扱わないでください。
  • ピン留めはオプトインです。 空のピン集合ではピン留めが無効になります。空の集合は、安定した既知の証明書チェーンでのみ使用し、ピン留めを行ったらバックアップピンを保持してください。
  • fontFiles には R2 バケットが必要です。 fontFiles 引数は、設定で r2FontBucket が設定されている場合にのみ意味があります。それ以外の場合は効果がありません。
  • ブリッジは署名を行いません。 PDF バイト列だけを返します。エッジでレンダリングしたうえで、自身のプロセス内で署名してください。これにより、署名鍵がエッジ境界を越えることはありません。

エッジレンダリングは、ブラウザーのコストをホストから完全に切り離します。代わりに発生するコストは、Worker への 1 回の HTTPS ラウンドトリップと Worker 自体のレンダリング時間です。これは結果に renderTimeMs として報告されます。ブリッジは、設定されたタイムアウトをピン留めされたトランスポート経由で適用します。実測した Worker のレイテンシーに余裕を加えて設定し、上流ゲートウェイのタイムアウトを下回るようにしてください。このパッケージは、自身が強制する制限のみを明記します。Cloudflare プラットフォームの CPU、メモリ、リクエストボディの上限については、いかなる主張も行いません。それらについては、Cloudflare のドキュメントとご自身の Worker を確認してください。

  • 宛先は、リクエストが PHP を離れる前に検証されます。 HTTPS 以外の URL は拒否されます。プライベートまたは予約済みのアドレス空間に解決されるホストは、すべての A レコードと AAAA レコードにわたって拒否されます。DNS リバインディングを防御するため、接続の直前にホストが再解決されます。
  • ピン留めされたトランスポートは DNS と TLS を固定します。 レスポンスファクトリとピンが設定されている場合、ブリッジは接続を検証済みの IP に固定し、SPKI ピン留めを強制し、ピアとホストを検証し、検証されていないホストへのリダイレクトには従いません。
  • 入力には上限があります。 maxHtmlSize(既定 5 MB)を超える HTML、過大な base64 データ URI、および <meta http-equiv="refresh"> タグは、リクエストの送信前に拒否されます。
  • シークレットは秘匿され、不変です。 apiToken と R2 鍵には #[SensitiveParameter] が付与されているため、スタックトレースから秘匿されます。また、設定オブジェクトは final readonly です。シークレットは環境またはシークレットマネージャーから取得し、決してコミットしないでください。
  • 空の catch ブロックは決して書かないでください。 各例は、特定の例外型をキャッチし、ログを記録するか、定義された終了コードで終了します。

完全なセキュリティモデル(SSRF と DNS リバインディングの防御、ピン留めの運用ガイダンス、シークレットの取り扱い方針)は、「関連項目」にリンクされている Cloudflare のセキュリティと運用ページにあり、関連する OWASP と RFC 7469 の条項に結び付けられています。

このガイド自体は、規範的な標準への準拠を主張しません。上流の Cloudflare のセキュリティと運用および設定ページでは、ブリッジの全レコード DNS 解決と TOCTOU 再チェックを OWASP の SSRF 防止ガイダンスに、TLS 公開鍵ピン留めとバックアップピンによる復旧を RFC 7469 にマッピングしています。このクックブックページは使用方法を再掲し、引用はそれらのページに委ねます。ブリッジは署名を行わず、署名準拠に関する主張も行いません。