Working with the YouTube Data API v3 is essential for video platforms like TrendVidStream. But the API has quirks: rate limits, quota exhaustion, intermittent failures, and region-specific errors. Here's how to handle them all.
Common Error Types
| HTTP Code | Error | Cause | Recovery |
|---|---|---|---|
| 400 | Bad Request | Invalid parameter | Fix request |
| 403 | Forbidden | Quota exceeded or API key invalid | Wait or rotate key |
| 404 | Not Found | Video/resource deleted | Skip and log |
| 429 | Too Many Requests | Rate limit hit | Exponential backoff |
| 500 | Internal Server Error | YouTube backend issue | Retry with backoff |
| 503 | Service Unavailable | Temporary outage | Retry with backoff |
Error-Resilient YouTube Client
<?php
class YouTubeClient
{
private string $apiKey;
private int $maxRetries;
private int $quotaUsed = 0;
private const DAILY_QUOTA = 10000; // Default free tier
public function __construct(string $apiKey, int $maxRetries = 3)
{
$this->apiKey = $apiKey;
$this->maxRetries = $maxRetries;
}
public function getTrending(string $region, int $maxResults = 50): array
{
return $this->request('videos', [
'part' => 'snippet,contentDetails,statistics',
'chart' => 'mostPopular',
'regionCode' => $region,
'maxResults' => $maxResults,
]);
}
public function getCategories(string $region): array
{
return $this->request('videoCategories', [
'part' => 'snippet',
'regionCode' => $region,
]);
}
private function request(string $endpoint, array $params): array
{
$params['key'] = $this->apiKey;
$url = "https://www.googleapis.com/youtube/v3/{$endpoint}?" . http_build_query($params);
$attempt = 0;
$lastError = null;
while ($attempt < $this->maxRetries) {
$attempt++;
try {
$result = $this->doRequest($url);
$this->trackQuota($endpoint);
return $result;
} catch (YouTubeQuotaException $e) {
// Quota exceeded - do not retry
error_log("YouTube quota exceeded: {$e->getMessage()}");
throw $e;
} catch (YouTubeRateLimitException $e) {
// Rate limited - backoff and retry
$delay = $this->calculateBackoff($attempt);
error_log("YouTube rate limited, waiting {$delay}s (attempt {$attempt})");
sleep($delay);
$lastError = $e;
} catch (YouTubeServerException $e) {
// Server error - backoff and retry
$delay = $this->calculateBackoff($attempt);
error_log("YouTube server error, waiting {$delay}s (attempt {$attempt})");
sleep($delay);
$lastError = $e;
} catch (YouTubeClientException $e) {
// Client error - do not retry
error_log("YouTube client error: {$e->getMessage()}");
throw $e;
}
}
throw new YouTubeException(
"Failed after {$this->maxRetries} attempts: " . $lastError?->getMessage()
);
}
private function doRequest(string $url): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
throw new YouTubeServerException("cURL error: $curlError");
}
$data = json_decode($response, true);
if ($httpCode === 403) {
$reason = $data['error']['errors'][0]['reason'] ?? 'unknown';
if ($reason === 'quotaExceeded' || $reason === 'dailyLimitExceeded') {
throw new YouTubeQuotaException("Quota exceeded: $reason");
}
throw new YouTubeClientException("Forbidden: $reason");
}
if ($httpCode === 429) {
throw new YouTubeRateLimitException('Rate limit exceeded');
}
if ($httpCode >= 500) {
throw new YouTubeServerException("Server error: $httpCode");
}
if ($httpCode >= 400) {
$message = $data['error']['message'] ?? "HTTP $httpCode";
throw new YouTubeClientException($message);
}
return $data;
}
private function calculateBackoff(int $attempt): int
{
// Exponential backoff with jitter
$base = min(pow(2, $attempt), 32);
return $base + random_int(0, $base);
}
private function trackQuota(string $endpoint): void
{
// Approximate quota costs
$costs = [
'videos' => 1,
'videoCategories' => 1,
'search' => 100,
'channels' => 1,
];
$this->quotaUsed += $costs[$endpoint] ?? 1;
}
public function getQuotaUsed(): int
{
return $this->quotaUsed;
}
public function isQuotaAvailable(): bool
{
return $this->quotaUsed < self::DAILY_QUOTA;
}
}
Exception Hierarchy
<?php
class YouTubeException extends \RuntimeException {}
class YouTubeClientException extends YouTubeException {} // 4xx errors
class YouTubeServerException extends YouTubeException {} // 5xx errors
class YouTubeQuotaException extends YouTubeClientException {} // Quota exceeded
class YouTubeRateLimitException extends YouTubeClientException {} // Rate limited
Usage in Cron Fetcher
<?php
$client = new YouTubeClient(getenv('YOUTUBE_API_KEY'));
$regions = ['US', 'GB', 'AE', 'DK', 'FI', 'CH', 'BE', 'CZ'];
foreach ($regions as $region) {
if (!$client->isQuotaAvailable()) {
echo "Quota exhausted, stopping.\n";
break;
}
try {
$videos = $client->getTrending($region);
$count = count($videos['items'] ?? []);
echo "[$region] Fetched $count videos\n";
foreach ($videos['items'] as $item) {
$db->upsertVideo(normalizeVideo($item, $region));
}
} catch (YouTubeQuotaException $e) {
echo "Quota exceeded, stopping all fetches.\n";
break;
} catch (YouTubeException $e) {
echo "[$region] Error: {$e->getMessage()}, skipping.\n";
continue;
}
// Rate limiting between regions
usleep(500000); // 500ms
}
echo "Quota used: {$client->getQuotaUsed()} units\n";
This error handling approach keeps TrendVidStream running reliably across all 8 regions, gracefully handling YouTube API issues without dropping data or crashing the fetch pipeline.
Top comments (0)