PHP — error handling
<?php
final class InScrapeException extends RuntimeException
{
public function __construct(public readonly int $status, public readonly string $code, string $message)
{
parent::__construct($status . ' ' . $code . ': ' . $message);
}
}
function inscrape_call(string $path, array $params, int $attempts = 3): array
{
for ($attempt = 0; $attempt < $attempts; $attempt++) {
$url = 'https://api.socialscrape.dev/v1' . $path . '?' . http_build_query($params);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('INSCRAPE_KEY')],
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($raw !== false) {
$body = json_decode($raw, true);
if ($status === 200) {
return $body;
}
// 4xx will fail identically on every retry. Only 5xx is worth another attempt,
// and only a 200 is ever charged.
if ($status < 500) {
throw new InScrapeException($status, $body['error']['code'], $body['error']['message']);
}
}
if ($attempt < $attempts - 1) {
sleep(2 ** $attempt);
}
}
throw new InScrapeException(500, 'SCRAPE_FAILED', 'gave up after ' . $attempts . ' attempts');
}
try {
$body = inscrape_call('/instagram/profile', ['handle' => 'natgeo']);
if (($body['data']['profile'] ?? null) === null) {
echo $body['data']['profile'], ' credits charged: ', $body['credits_charged'], PHP_EOL;
} else {
echo $body['data']['profile']['follower_count'], ' credits left: ', $body['credits_remaining'], PHP_EOL;
}
} catch (InScrapeException $e) {
echo match ($e->code) {
'ENDPOINT_NOT_FOUND' => 'API route not found',
'INSUFFICIENT_CREDITS' => 'out of credits, top up before retrying',
'INVALID_API_KEY' => 'check INSCRAPE_KEY',
default => $e->getMessage(),
}, PHP_EOL;
}