Skip to content

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).

  • network_error, timeout, 5xx responses (such as 503 PUBLISH_IN_PROGRESS) and 429 responses (such as RATE_LIMIT_EXCEEDED) are retried.
  • QUOTA_EXCEEDED and OVERAGE_CAP_EXCEEDED (429) and STORAGE_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 and isRetryable() is false.
  • DOWNLOAD_FAILED is never retried and isRetryable() is false for it, whatever the storage status.
  • invalid_response (a 2xx whose body is not JSON, usually from a proxy) and io_error (renderToFile() could not write the file) are never retried: both would fail the same way again.
  • Up to maxRetries retries (default 2 — three total attempts); after the last one, the last error is thrown.
  • Before retry n (1-based) the SDK waits retryDelay × 2^(n-1) × j seconds (retryDelay defaults to 0.5), with j a random factor in [0.5, 1.5). The wait is never shorter than that.
  • Honors the Retry-After response header when present (integer seconds or an HTTP-date), clamped to 0–30 seconds, without jitter. Retry-After-Ms is 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 past timeout a timeout, and both are retried.
  • The presigned-URL download (downloadPdf(), and the second hop of render->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.

Set maxRetries: 0 on the constructor to disable retries entirely.

$client = new PoliPage(
apiKey: $_ENV['POLI_PAGE_API_KEY'],
maxRetries: 0,
);

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.

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