Table of Contents
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
- Speed first: APIs are faster and cleaner than HTML parsing. Try that first.
- Graceful degradation: If the API is down, fall back to a slower but reliable method.
- User experience: Users get data either way—they don’t see errors.
- 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.
Leave a Reply