Troubleshooting
At a glance
Section titled “At a glance”The bridge raises three exception types. The exception you catch tells you what failed and whether to retry or use a fallback. Each message fragment below comes from the source.
The exception hierarchy
Section titled “The exception hierarchy”| Exception | Extends | Meaning |
|---|---|---|
CloudflareNotAvailableException | NextPDF\Exception\NextPdfException | The bridge cannot reach the edge, or config is incomplete, and no usable fallback exists. |
CloudflareRenderException | NextPDF\Exception\NextPdfException | The Worker answered, but the render failed (Hypertext Transfer Protocol (HTTP) error or malformed body). Never falls back. |
InvalidSpkiPinException | InvalidArgumentException | A configured Subject Public Key Info (SPKI) pin string is malformed. |
CloudflareSecurityPolicy also raises RuntimeException directly for
input and Uniform Resource Locator (URL) policy violations. It raises
the exception before it sends any request.
Configuration and input failures
Section titled “Configuration and input failures”| Message fragment | Raised by | Cause | Fix |
|---|---|---|---|
incomplete (missing worker_url or api_token) | Renderer (via fallback path) | Either workerUrl or apiToken is empty | Set both, then verify with isValid(). |
HTML input exceeds maximum size | CloudflareSecurityPolicy::validate() | The Hypertext Markup Language (HTML) input is longer than maxHtmlSize | Reduce the input, or raise maxHtmlSize deliberately. |
Base64 data URI exceeds safety limit | CloudflareSecurityPolicy::validate() | A data:;base64, Uniform Resource Identifier (URI) is estimated above 13631488 bytes | Externalize the asset; do not inline large binaries. |
meta-refresh redirect which could cause SSRF | CloudflareSecurityPolicy::validate() | A <meta http-equiv="refresh"> tag could trigger server-side request forgery (SSRF) | Remove the tag; use a server-side redirect outside the rendered HTML. |
Invalid Worker URL | validateWorkerUrl() | The URL does not parse or lacks scheme/host | Provide a full absolute URL that uses Hypertext Transfer Protocol Secure (HTTPS). |
Worker URL must use HTTPS | validateWorkerUrl() | Scheme is not HTTPS | Use https://. |
private or reserved IP addresses | validateWorkerUrl() | Internet Protocol (IP) literal in Request for Comments (RFC) 1918 / loopback / RFC 3927 range | Point at a public endpoint. |
hostname resolves to a private or reserved IP | validateWorkerUrl() | A resolved Domain Name System (DNS) A/AAAA record is private or reserved | Fix DNS; investigate possible rebinding. |
DNS answer changed since validation | assertPinsStillValid() | The host resolved to a new IP address between check and send | Re-resolve; treat as a possible rebinding attempt. |
Worker-side failures
Section titled “Worker-side failures”These are CloudflareRenderException failures. The Worker answered, but
the render itself failed. These never trigger the local fallback
because the edge was reachable.
| Message fragment | Cause |
|---|---|
Cloudflare Worker returned HTTP <code>: <detail> | Non-200 status. Detail comes from the JavaScript Object Notation (JSON) error field or the first 200 body bytes. |
Worker returned empty or invalid PDF data | Binary response does not start with %PDF. |
Worker error: <message> | JSON response that carries an error field. |
JSON response missing "pdf" field | JSON response without a pdf field. |
Invalid base64-encoded PDF in JSON response | The pdf field did not base64-decode to bytes starting with %PDF. |
Invalid JSON response from Worker | Body uses Content-Type: application/json, but does not decode to an array. |
Unexpected Content-Type from Worker: <type> | A 200 response whose Content-Type is neither application/pdf nor application/json. |
When you catch one of these, inspect the Worker logs. The failure is on the Worker side, not in this bridge.
Reachability and fallback failures
Section titled “Reachability and fallback failures”These are CloudflareNotAvailableException failures. The bridge could
not use the edge, and no fallback produced a Portable Document Format
(PDF) file.
| Message fragment | Cause | Fix |
|---|---|---|
Cloudflare Worker unavailable: <reason> | Transport error with fallback disabled | Enable fallbackToLocal and wire a factory, or fix connectivity. |
Artisan is installed but no LocalRendererFactoryInterface was provided | nextpdf/artisan is present, but no factory was passed | Pass a LocalRendererFactoryInterface to the constructor. |
local Chrome fallback (nextpdf/artisan) is not installed | Fallback is enabled, no factory is configured, and Artisan is absent | Run composer require nextpdf/artisan, then wire a factory. |
When you supply a PHP Standards Recommendation (PSR)-3 logger and the
fallback path runs, the bridge logs a warning (Cloudflare render failed, attempting fallback), then an info (Falling back to local renderer).
Transport / pinning failures
Section titled “Transport / pinning failures”| Symptom | Cause | Fix |
|---|---|---|
InvalidSpkiPinException: Invalid SPKI pin format | A pin is not in sha256/<base64> (or sha256//<base64>) form | Correct the pin string. |
cURL transport error (<n>): <msg> | cURL-level failure (Transport Layer Security (TLS), DNS, timeout) | Inspect the cURL error number; if pins are set, confirm the served SPKI is still pinned. |
| Renders fail immediately after certificate rotation | The new certificate’s SPKI is not in the pin set | Add the new SPKI as a backup pin before rotating. |
| Pinned transport not used despite pins configured | No PSR-17 ResponseFactory was supplied | Pass a ResponseFactory; the pinned transport requires it. |
isAvailable() behavior
Section titled “isAvailable() behavior”isAvailable() never throws. It returns false when configuration is
invalid or when the HEAD probe fails or raises an exception. It returns
true only when the probe responds with a status below 500. A true
result is only a hint: the subsequent POST can still fail with any of
the Worker-side errors above. Do not treat a passing probe as a guarantee.
Rate-limit surprises
Section titled “Rate-limit surprises”ApiProtection keeps limits in memory per process. Counts do not survive
a restart, and they are not shared across workers or nodes. If one node
allows a client and another denies it, that is expected. Put a shared
store in front of the limiter for a cluster-wide limit.
See also
Section titled “See also”- /integrations/cloudflare/security-and-operations/ — the operational runbook and controls behind these messages.
- /integrations/cloudflare/quickstart/ — the canonical try/catch pattern.
- /integrations/cloudflare/production-usage/ — fallback wiring details.