Building Resilient API Fallback Chains in Laravel

📖 4 minutes read

External APIs fail. Networks timeout. Services go down for maintenance. Your app needs to handle this gracefully instead of showing error pages to users.

One approach: build fallback chains. Try the fastest/best method first, then degrade to alternatives when things break.

The Pattern

use Illuminate\Support\Facades\Http;

class ContentFetcher
{
    public function fetchArticle(string $slug): ?array
    {
        // Strategy 1: Try the clean REST API first
        $response = Http::timeout(5)->get("https://api.news.com/articles", [
            'slug' => $slug,
            'format' => 'json',
        ]);

        if ($response->successful() && !empty($response->json())) {
            return $this->normalizeApiResponse($response->json());
        }

        // Strategy 2: Fallback to HTML scraping
        Log::warning("API unavailable for {$slug}, using HTML fallback");

        $response = Http::timeout(10)->get("https://news.com/articles/{$slug}");

        if ($response->failed()) {
            Log::error("All fetch strategies failed for {$slug}");
            return null;
        }

        // Validate we got HTML, not an error page
        if (!str_contains($response->header('Content-Type'), 'text/html')) {
            return null;
        }

        return $this->parseHtml($response->body());
    }

    private function normalizeApiResponse(array $data): array
    {
        return [
            'title' => $data['title'],
            'body' => $data['content'],
            'author' => $data['author']['name'],
            'published_at' => $data['published'],
        ];
    }

    private function parseHtml(string $html): ?array
    {
        // Your HTML parsing logic here
        // DOMDocument, Symfony DomCrawler, or Laravel AI for extraction
        return [...];
    }
}

Why This Works

  1. Speed first: APIs are faster and cleaner than HTML parsing. Try that first.
  2. Graceful degradation: If the API is down, fall back to a slower but reliable method.
  3. User experience: Users get data either way—they don’t see errors.
  4. Observability: Log fallback usage so you can monitor API reliability.

Validation at Each Step

Don’t assume a 200 status code means success. Validate the response:

// ❌ WRONG: Assumes 200 = valid data
if ($response->successful()) {
    return $response->json();
}

// ✅ RIGHT: Check the data structure
if ($response->successful()) {
    $data = $response->json();
    
    // Verify required fields exist
    if (empty($data['id']) || empty($data['title'])) {
        Log::warning('API returned malformed data', ['response' => $data]);
        return $this->tryFallback();
    }
    
    return $data;
}

Adding Timeouts

Use aggressive timeouts for fallback strategies. If the API is slow, you want to fail fast and move to the next option:

// Primary: 5 second timeout (should be fast)
$response = Http::timeout(5)->get($apiUrl);

if ($response->failed() || $response->timedOut()) {
    // Fallback: 10 second timeout (HTML parsing takes longer)
    $response = Http::timeout(10)->get($htmlUrl);
}

Caching Across Strategies

Cache the result after choosing a strategy, so future requests skip the fallback entirely:

use Illuminate\Support\Facades\Cache;

public function fetchArticle(string $slug): ?array
{
    return Cache::remember("article:{$slug}", 3600, function () use ($slug) {
        // Try API first
        $data = $this->tryApi($slug);
        
        if ($data !== null) {
            return $data;
        }
        
        // Fallback to HTML
        return $this->tryHtml($slug);
    });
}

Now subsequent requests use the cached result regardless of which strategy succeeded.

Multiple Fallback Levels

You can chain more than two strategies:

public function fetchData(string $id): ?array
{
    // Level 1: Fast API
    $data = $this->tryFastApi($id);
    if ($data) return $data;

    // Level 2: Slower API with more features
    $data = $this->trySlowApi($id);
    if ($data) return $data;

    // Level 3: HTML scraping
    $data = $this->tryHtmlScrape($id);
    if ($data) return $data;

    // Level 4: Stale cached data (last resort)
    return Cache::get("article:{$id}:stale");
}

When NOT to Use This Pattern

Fallback chains add complexity. Don’t use them if:

  • The API is reliable (99.9%+ uptime)
  • There’s no reasonable fallback (you need that specific API’s data)
  • Fallback data quality is too degraded to be useful

For critical integrations, consider a different approach: retry with exponential backoff, or queue the request for later processing.

Monitoring Fallback Usage

Track how often fallbacks trigger:

if ($this->tryApi($slug) === null) {
    Metrics::increment('api.fallback.triggered', ['service' => 'news-api']);
    return $this->tryHtml($slug);
}

If fallbacks fire frequently, it’s a signal to investigate the primary API’s reliability or adjust timeouts.

The Bottom Line

Fallback chains make your app resilient. Instead of failing when an API hiccups, gracefully degrade to alternative data sources. Users stay happy, and you get telemetry on when primary services are unreliable.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *