Invalid Date

When you integrate a PDF generation API into production, you inevitably encounter errors that never appeared during development. Network timeouts, rate limiting during traffic spikes, rendering failures from complex HTML — if you don't handle these properly, users end up with broken PDFs or batch jobs that silently stop midway.

This guide covers the most common error patterns in PDF APIs like FUNBREW PDF, along with practical retry strategies using exponential backoff. Code examples in curl, Python, Node.js, and PHP will help you build resilient error handling from day one.

For error handling in large-scale batch jobs, see the PDF Batch Processing Guide. API key security is covered in the Security Guide. If you are new to the API, the HTML-to-PDF Complete Guide is a good starting point, and the Quickstart by Language gets you to your first working request in minutes. For Markdown-specific error handling, see the Markdown to PDF API Guide.

Common Error Patterns

1. Timeouts (408 / 504)

Complex HTML or image-heavy templates take longer to render. Combined with network latency, this can trigger client-side or server-side timeouts.

Cause Solution
Slow external resource loading (images, fonts) Inline with Base64 or use a CDN
Complex CSS (gradients, shadow DOM) Simplify layout
Client timeout set too short Set timeout to 60–120 seconds

2. Rate Limiting (429 Too Many Requests)

Sending too many requests in a short window causes the API to return 429. If the response includes a Retry-After header, use that value as the wait time.

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711900800

3. Invalid HTML (400 Bad Request)

Malformed HTML causes rendering engine crashes or degraded output quality.

  • Unclosed tags (<div> without matching </div>)
  • Invalid Base64-encoded images
  • References to external CSS that returns 404

For CSS-related rendering problems, see PDF-Specific CSS Layout Tips. If you are migrating from wkhtmltopdf and seeing different rendering behaviour, the wkhtmltopdf vs Chromium Comparison explains the key differences to watch out for. For general output troubleshooting, see the Troubleshooting Guide.

4. Authentication Errors (401 / 403)

These occur when the API key is invalid, expired, or lacks the required scope. Retrying won't fix this — alert immediately and rotate the key.

5. Server Errors (500 / 502 / 503)

Transient server-side issues. Treat as retryable and apply exponential backoff.

Retry Strategy: Exponential Backoff Basics

Retrying at a fixed interval concentrates load on the server. Exponential backoff increases the wait time exponentially and adds random jitter to spread out requests.

wait_time = min(initial_delay × 2^attempt + random_jitter, max_delay)
Attempt Example wait time (initial=1s, max=60s)
1st 1–3 seconds
2nd 2–6 seconds
3rd 4–12 seconds
4th 8–24 seconds
5th 16–48 seconds

Which Errors to Retry

Status Code Retry? Reason
408, 429, 500, 502, 503, 504 Yes Transient issues
400 No Request itself is malformed
401, 403 No Auth issues won't be resolved by retrying
404 No Resource does not exist

Code Examples

curl (Shell Script)

#!/bin/bash

API_URL="https://pdf.funbrew.cloud/api/v1/generate"
API_KEY="your-api-key"
MAX_RETRIES=5
INITIAL_DELAY=1

generate_pdf_with_retry() {
  local payload="$1"
  local attempt=0
  local delay=$INITIAL_DELAY

  while [ $attempt -le $MAX_RETRIES ]; do
    response=$(curl -s -w "\n%{http_code}" \
      -X POST "$API_URL" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Content-Type: application/json" \
      -d "$payload" \
      --max-time 120)

    http_code=$(echo "$response" | tail -1)
    body=$(echo "$response" | head -n -1)

    case $http_code in
      200)
        echo "$body"
        return 0
        ;;
      400|401|403|404)
        echo "Non-retryable error: $http_code" >&2
        echo "$body" >&2
        return 1
        ;;
      408|429|500|502|503|504)
        attempt=$((attempt + 1))
        if [ $attempt -gt $MAX_RETRIES ]; then
          echo "Max retries reached" >&2
          return 1
        fi
        jitter=$((RANDOM % 1000))
        wait_ms=$(( delay * 1000 + jitter ))
        wait_sec=$(echo "scale=3; $wait_ms / 1000" | bc)
        echo "Retry $attempt/$MAX_RETRIES: waiting ${wait_sec}s (HTTP $http_code)" >&2
        sleep "$wait_sec"
        delay=$((delay * 2))
        [ $delay -gt 60 ] && delay=60
        ;;
    esac
  done
}

PAYLOAD='{"html": "<h1>Invoice</h1>", "options": {"format": "A4"}}'
generate_pdf_with_retry "$PAYLOAD"

Python

import time
import random
import requests
from dataclasses import dataclass
from typing import Optional

@dataclass
class RetryConfig:
    max_retries: int = 5
    initial_delay: float = 1.0
    max_delay: float = 60.0
    backoff_multiplier: float = 2.0
    jitter_range: float = 1.0

RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}

def generate_pdf_with_retry(
    html: str,
    api_key: str,
    options: Optional[dict] = None,
    config: Optional[RetryConfig] = None,
) -> bytes:
    """Generate a PDF with exponential backoff retry."""
    if config is None:
        config = RetryConfig()

    url = "https://pdf.funbrew.cloud/api/v1/generate"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    payload = {"html": html, "options": options or {"format": "A4"}}

    last_exception = None
    delay = config.initial_delay

    for attempt in range(config.max_retries + 1):
        try:
            response = requests.post(url, json=payload, headers=headers, timeout=120)

            if response.status_code == 200:
                return response.content

            if response.status_code not in RETRYABLE_STATUS_CODES:
                error_info = response.json() if response.content else {}
                raise ValueError(
                    f"Non-retryable error: HTTP {response.status_code} - {error_info}"
                )

            # Honour Retry-After for rate limiting
            if response.status_code == 429:
                retry_after = response.headers.get("Retry-After")
                if retry_after:
                    delay = float(retry_after)

        except requests.exceptions.Timeout as e:
            last_exception = e
        except requests.exceptions.ConnectionError as e:
            last_exception = e

        if attempt >= config.max_retries:
            break

        jitter = random.uniform(0, config.jitter_range)
        wait_time = min(delay + jitter, config.max_delay)
        print(f"Retry {attempt + 1}/{config.max_retries}: waiting {wait_time:.2f}s")
        time.sleep(wait_time)
        delay = min(delay * config.backoff_multiplier, config.max_delay)

    raise RuntimeError(
        f"Max retries ({config.max_retries}) exceeded"
    ) from last_exception


# Usage
try:
    pdf_bytes = generate_pdf_with_retry(
        html="<h1>Invoice</h1><p>Total: $100.00</p>",
        api_key="your-api-key",
        options={"format": "A4", "margin": {"top": "20mm"}},
    )
    with open("invoice.pdf", "wb") as f:
        f.write(pdf_bytes)
    print("PDF generated successfully")
except ValueError as e:
    print(f"Input error (fix required): {e}")
except RuntimeError as e:
    print(f"Server error (retry later): {e}")

Node.js

const axios = require('axios');

const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);

/**
 * Generate a PDF with exponential backoff retry.
 */
async function generatePdfWithRetry(html, apiKey, options = {}, retryConfig = {}) {
  const {
    maxRetries = 5,
    initialDelay = 1000,   // milliseconds
    maxDelay = 60000,
    backoffMultiplier = 2,
  } = retryConfig;

  let delay = initialDelay;
  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await axios.post(
        'https://pdf.funbrew.cloud/api/v1/generate',
        { html, options: { format: 'A4', ...options } },
        {
          headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
          responseType: 'arraybuffer',
          timeout: 120000,
        }
      );
      return Buffer.from(response.data);

    } catch (error) {
      const status = error.response?.status;

      if (status && !RETRYABLE_STATUS_CODES.has(status)) {
        const errorBody = error.response?.data
          ? JSON.parse(Buffer.from(error.response.data).toString())
          : {};
        throw new Error(`Non-retryable error: HTTP ${status} - ${JSON.stringify(errorBody)}`);
      }

      lastError = error;
      if (attempt >= maxRetries) break;

      let waitTime = delay;
      if (status === 429) {
        const retryAfter = error.response?.headers?.['retry-after'];
        if (retryAfter) waitTime = parseFloat(retryAfter) * 1000;
      }

      const jitter = Math.random() * 1000;
      const actualWait = Math.min(waitTime + jitter, maxDelay);
      console.warn(`Retry ${attempt + 1}/${maxRetries}: waiting ${(actualWait / 1000).toFixed(2)}s`);

      await new Promise((resolve) => setTimeout(resolve, actualWait));
      delay = Math.min(delay * backoffMultiplier, maxDelay);
    }
  }

  throw new Error(`Max retries (${maxRetries}) exceeded`, { cause: lastError });
}

// Usage
generatePdfWithRetry(
  '<h1>Invoice</h1><p>Total: $100.00</p>',
  'your-api-key',
  { format: 'A4' },
  { maxRetries: 3, initialDelay: 2000 }
)
  .then((buf) => {
    require('fs').writeFileSync('invoice.pdf', buf);
    console.log('PDF generated successfully');
  })
  .catch((err) => console.error('Error:', err.message));

PHP

<?php

class PdfApiClient
{
    private const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];

    public function __construct(
        private string $apiKey,
        private string $baseUrl = 'https://pdf.funbrew.cloud/api/v1',
        private int $maxRetries = 5,
        private float $initialDelay = 1.0,
        private float $maxDelay = 60.0,
        private float $backoffMultiplier = 2.0,
    ) {}

    /**
     * Generate a PDF with exponential backoff retry.
     *
     * @throws \RuntimeException When max retries are exhausted
     * @throws \InvalidArgumentException For non-retryable errors
     */
    public function generateWithRetry(string $html, array $options = []): string
    {
        $delay = $this->initialDelay;
        $lastError = null;

        for ($attempt = 0; $attempt <= $this->maxRetries; $attempt++) {
            try {
                return $this->callApi($html, $options);
            } catch (\RuntimeException $e) {
                $statusCode = $e->getCode();

                if (!in_array($statusCode, self::RETRYABLE_STATUS_CODES, true)) {
                    throw new \InvalidArgumentException(
                        "Non-retryable error: HTTP {$statusCode} - " . $e->getMessage(),
                        $statusCode
                    );
                }

                $lastError = $e;
                if ($attempt >= $this->maxRetries) break;

                $jitter = mt_rand(0, 1000) / 1000;
                $waitTime = min($delay + $jitter, $this->maxDelay);
                error_log("Retry " . ($attempt + 1) . "/{$this->maxRetries}: waiting {$waitTime}s");
                usleep((int) ($waitTime * 1_000_000));
                $delay = min($delay * $this->backoffMultiplier, $this->maxDelay);
            }
        }

        throw new \RuntimeException(
            "Max retries ({$this->maxRetries}) exceeded: " . $lastError?->getMessage(),
            0,
            $lastError
        );
    }

    private function callApi(string $html, array $options): string
    {
        $ch = curl_init("{$this->baseUrl}/generate");
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode([
                'html' => $html,
                'options' => array_merge(['format' => 'A4'], $options),
            ]),
            CURLOPT_HTTPHEADER => [
                "Authorization: Bearer {$this->apiKey}",
                'Content-Type: application/json',
            ],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 120,
        ]);

        $body = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError) {
            throw new \RuntimeException("cURL error: {$curlError}", 0);
        }

        if ($statusCode === 200) {
            return $body;
        }

        throw new \RuntimeException(
            json_decode($body, true)['message'] ?? 'Unknown error',
            $statusCode
        );
    }
}

// Usage
$client = new PdfApiClient(apiKey: 'your-api-key');

try {
    $pdf = $client->generateWithRetry(
        html: '<h1>Invoice</h1><p>Total: $100.00</p>',
        options: ['format' => 'A4']
    );
    file_put_contents('invoice.pdf', $pdf);
    echo "PDF generated successfully\n";
} catch (\InvalidArgumentException $e) {
    echo "Input error (fix required): " . $e->getMessage() . "\n";
} catch (\RuntimeException $e) {
    echo "Server error (retry later): " . $e->getMessage() . "\n";
}

Error Logging Best Practices

Good logging makes it easy to detect problems early and pinpoint root causes.

What to Include in Logs

{
  "timestamp": "2026-03-31T12:00:00Z",
  "level": "error",
  "event": "pdf_generation_failed",
  "request_id": "req_abc123",
  "attempt": 3,
  "max_retries": 5,
  "status_code": 503,
  "error_message": "Service Unavailable",
  "html_size_bytes": 15420,
  "duration_ms": 5230,
  "will_retry": true
}

Log Level Guidelines

Situation Log Level Action
Retryable error (in progress) warn No alert needed
Max retries reached error Trigger alert
Auth error (401/403) error Immediate alert
Input error (400) warn Code fix needed
Success (after retries) info Record only

FUNBREW PDF-Specific Tips

Once your retry logic is in place, consider adding Webhook notifications so your system is alerted the moment a generation job fails — see the Webhook Integration Guide. For real-world error handling patterns in production workflows, the Invoice PDF Automation Guide and Certificate PDF Automation Guide both include failure-recovery strategies. For Next.js/Nuxt-specific error handling patterns, see the Next.js & Nuxt PDF API Guide.

Handling Timeouts

If your HTML references external resources, inline them as Base64 to eliminate load time.

{
  "html": "<img src='data:image/png;base64,iVBORw0KGgo...' />",
  "options": {
    "format": "A4",
    "waitUntil": "networkidle0"
  }
}

Avoiding Rate Limits

Use batch processing to generate multiple PDFs in a single request, reducing your total API call count.

{
  "batch": [
    { "html": "<h1>Invoice #001</h1>", "filename": "invoice-001.pdf" },
    { "html": "<h1>Invoice #002</h1>", "filename": "invoice-002.pdf" }
  ]
}

Rotating API Keys

If authentication errors persist, regenerate your API key from the dashboard. Always use separate keys for production and staging. Rate limits vary by plan, so if you are hitting 429s frequently, check the Pricing Comparison to find a plan that fits your volume.

Browse real-world automation examples — invoices at /use-cases/invoices, reports at /use-cases/reports, and more at the full Use Cases gallery.

Summary

Key takeaways for robust PDF API error handling:

  1. Classify errors: Distinguish retryable (some 4xx, all 5xx) from non-retryable (400, 401, 403)
  2. Use exponential backoff: Increase wait time exponentially, add jitter
  3. Set limits: Always define max retries and max wait time
  4. Log everything: Record request ID, status code, and retry count
  5. Alert on critical failures: Max retries reached and auth errors should page someone

For the full production readiness picture — including error handling, monitoring, and scaling — see the PDF API Production Checklist. Check the FUNBREW PDF API reference for the full list of error codes and recommended responses. Use the playground to test your HTML before integrating. To see certificate or invoice generation in action, visit the Use Cases gallery. For serverless PDF generation on AWS Lambda, see the Serverless PDF Guide. Django/FastAPI integration is covered in the Django/FastAPI Guide. For migration-specific error patterns, see the Puppeteer Migration Guide. Report generation error handling is in the Report PDF Generation Guide. Compare PDF API error handling approaches in the PDF API Comparison.

Powered by FUNBREW PDF