Retries and idempotency
The Poli Page Go 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 returns them at once andIsRetryable()isfalse.DOWNLOAD_FAILEDis never retried andIsRetryable()isfalsefor it, whatever the storage status.- Up to
option.WithMaxRetries(n)retries (default 2 — three total attempts); after the last one, the last error is returned. - Exponential backoff: before retry
n(1-based) the SDK waitsRetryDelay × 2^(n−1) × j, withja random factor in[0.5, 1.5)(option.WithRetryDelay(d), default500ms). - Honors the
Retry-Afterresponse header when present (integer seconds or an HTTP-date), clamped to 0–30 s, without jitter.Retry-After-Msis ignored. Any otherRetry-Aftervalue falls back to the backoff. - A wait is never shorter than its delay.
aborted(context cancelled) is never retried, and a cancellation during a wait returns at once. The presigned-URL download is never retried.- Go’s HTTP transport may resend a GET once on a stale keep-alive connection; it never resends a POST. A POST whose connection dies surfaces as
network_errorand goes through the retry policy above. That includes, over HTTP/2, a POST the server refused before processing it (a refused stream or aGOAWAY): it uses one SDK retry, and withoption.WithMaxRetries(0)it is returned asnetwork_error. Such an attempt was never processed, so retrying it cannot duplicate a render. - A 2xx answer whose body is cut off or outlasts the timeout while it is read is a
network_errorortimeout, retried like the others.
Disabling retries
Section titled “Disabling retries”Pass option.WithMaxRetries(0) to disable retries entirely.
client := polipage.NewClient( option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")), option.WithMaxRetries(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 when 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 (for example option.WithTimeout(10*time.Second) on a 15 s render), 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 an extra stored document matters to you, lower option.WithMaxRetries or raise option.WithTimeout; note that option.WithMaxRetries(0) also stops the always-safe retry of PUBLISH_IN_PROGRESS.
You can supply your own key via option.WithIdempotencyKey as the last argument, for example to correlate a call in your logs; it is sent as is on every attempt of that call:
pdf, err := client.Render.PDF(ctx, polipage.RenderInput{ Project: "billing", Template: "invoice", Data: map[string]any{"invoiceNumber": "INV-001"},}, option.WithIdempotencyKey("invoice-INV-001-v1"))Two calls with the same key are still two renders: each stores its own document.
Example
Section titled “Example”package main
import ( "context" "os" "time"
"github.com/poli-page/sdk-go" "github.com/poli-page/sdk-go/option")
func main() { client := polipage.NewClient( option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")), option.WithMaxRetries(4), option.WithRetryDelay(250*time.Millisecond), )
if _, err := client.Render.PDF(context.Background(), polipage.RenderInput{ Project: "billing", Template: "invoice", Data: map[string]any{"invoiceNumber": "INV-001"}, }); err != nil { panic(err) }}