Skip to content

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

  • 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 returns them at once and IsRetryable() is false.
  • DOWNLOAD_FAILED is never retried and IsRetryable() is false for 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 waits RetryDelay × 2^(n−1) × j, with j a random factor in [0.5, 1.5) (option.WithRetryDelay(d), default 500ms).
  • Honors the Retry-After response header when present (integer seconds or an HTTP-date), clamped to 0–30 s, without jitter. Retry-After-Ms is ignored. Any other Retry-After value 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_error and goes through the retry policy above. That includes, over HTTP/2, a POST the server refused before processing it (a refused stream or a GOAWAY): it uses one SDK retry, and with option.WithMaxRetries(0) it is returned as network_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_error or timeout, retried like the others.

Pass option.WithMaxRetries(0) to disable retries entirely.

client := polipage.NewClient(
option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")),
option.WithMaxRetries(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 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.

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