Retries and idempotency
The Poli Page PHP SDK retries transient failures automatically. Every POST carries an Idempotency-Key that stays the same across the retries of one call. The API does not deduplicate on it, so a retried render can store a second document (see Idempotency).
Default retry policy
Section titled “Default retry policy”network_error,timeout, 5xx responses (such as503 PUBLISH_IN_PROGRESS) and 429 responses (such asRATE_LIMIT_EXCEEDED) are retried.QUOTA_EXCEEDEDandOVERAGE_CAP_EXCEEDED(429) andSTORAGE_ACCESS_DENIED(502) are not retried: they last until the monthly reset or until the bucket access is restored, so the SDK throws them at once andisRetryable()isfalse.DOWNLOAD_FAILEDis never retried andisRetryable()isfalsefor it, whatever the storage status.invalid_response(a 2xx whose body is not JSON, usually from a proxy) andio_error(renderToFile()could not write the file) are never retried: both would fail the same way again.- Up to
maxRetriesretries (default2— three total attempts); after the last one, the last error is thrown. - Before retry
n(1-based) the SDK waitsretryDelay × 2^(n-1) × jseconds (retryDelaydefaults to0.5), withja random factor in[0.5, 1.5). The wait is never shorter than that. - Honors the
Retry-Afterresponse header when present (integer seconds or an HTTP-date), clamped to 0–30 seconds, without jitter.Retry-After-Msis ignored. - An answer’s whole body is read inside its attempt: a connection dropped while it is read is a
network_error, a body that stalls pasttimeoutatimeout, and both are retried. - The presigned-URL download (
downloadPdf(), and the second hop ofrender->pdf()/render->pdfStream()) is never retried. aborted(caller-cancelled) is never retried.
The SDK’s retries are the only resends. With Guzzle or Symfony HttpClient, a POST always opens a fresh connection, so libcurl never resends it on its own when a reused keep-alive connection dies before any answer: that failure is a network_error, counted by maxRetries and reported to onRequest and onRetry like any other attempt. With Symfony HttpClient, that request option takes the place of any extra.curl default you set on your client, for the SDK’s POSTs only. With another PSR-18 client, check whether it resends requests itself.
Disabling retries
Section titled “Disabling retries”Set maxRetries: 0 on the constructor to disable retries entirely.
$client = new PoliPage( apiKey: $_ENV['POLI_PAGE_API_KEY'], maxRetries: 0,);Idempotency
Section titled “Idempotency”Every POST request (render->*, documents->thumbnails()) includes an Idempotency-Key header: a lowercase UUID v4 the SDK generates once per call and sends unchanged on every retry of that call. GET and DELETE requests and the presigned-URL download send none.
The API does not deduplicate on this header. Every POST /v1/render that reaches the renderer stores a new document, so a retry can create a second one whenever the first attempt was stored but its answer was lost: a 5xx raised after the document was stored, a timeout that fired while the API was still rendering, or a network_error after the request reached the API. A 503 PUBLISH_IN_PROGRESS is raised before any render work, so retrying it never duplicates. If a duplicate matters to you, set maxRetries: 0 on that call’s client and handle the failure yourself.
You can supply your own key via the input’s idempotencyKey parameter, for example to correlate a call in your logs; it is sent as is on every attempt of that call:
use PoliPage\RenderInput;
$pdf = $client->render->pdf(new RenderInput( project: 'billing', template: 'invoice', data: ['invoiceNumber' => 'INV-001'], idempotencyKey: 'invoice-INV-001-v1',));Two calls with the same key are still two renders: each stores its own document.
The key is sent as an HTTP header, so it must be a valid header value. A key with CR, LF, NUL or another control character other than tab fails before any request is built with a PoliPageException (invalid_options) whose message never contains the key. It is not retried.
Example
Section titled “Example”<?php
require __DIR__ . '/vendor/autoload.php';
use PoliPage\PoliPage;use PoliPage\RenderInput;
$client = new PoliPage( apiKey: $_ENV['POLI_PAGE_API_KEY'], maxRetries: 4, retryDelay: 0.25,);
$pdf = $client->render->pdf(new RenderInput( project: 'billing', template: 'invoice', data: ['invoiceNumber' => 'INV-001', 'total' => 1280],));