Errors and retries
How failed requests are reported, and which requests are retried.
Errors
Every error the SDK throws at runtime extends SynthientException, which extends \RuntimeException. Invalid arguments throw \InvalidArgumentException instead, before any request is made.
| Exception | Thrown when |
|---|---|
ApiException | The API, or the storage behind an export download, answered with an error status. |
ConnectionException | The request could not be completed because of a network failure. |
TimeoutException | The connection, the response, or a read from a stream or download took longer than the client's timeout. It extends ConnectionException. |
SynthientException | The response was malformed, for example invalid JSON, a missing or mistyped field, or a body over 16 MiB. It is also thrown for a stream or download through the default HTTP client when allow_url_fopen is off, and it is the base class of the others. |
use Synthient\Exception\ApiException;
try {
$info = $client->lookupIp('8.8.8.8');
echo $info->ip, "\n";
} catch (ApiException $e) {
if ($e->statusCode === 402) {
echo "out of lookup credits\n";
} elseif ($e->statusCode === 429 && $e->retryAfter !== null) {
echo "rate limited, retry after {$e->retryAfter} seconds\n";
} elseif ($e->statusCode === 429) {
echo "monthly request budget spent\n";
} else {
echo "request {$e->requestId} failed: ", $e->detail ?? $e->title, "\n";
}
}
- Cost: A billed lookup that fails with a
ConnectionExceptionmay or may not have been charged, so it is never retried automatically. - Security: Guzzle's own exception is never chained, because its request holds the API key.
- Response hook: An exception thrown by
responseHookreaches the caller unchanged.
Common statuses are:
- 401 for a missing or invalid key
- 402 when the team is out of lookup credits
- 403 when the key lacks a scope
- 404 while a daily export is still rolling up
- 422 for invalid input
- 429 when rate limited
See Errors for what each status means.
ApiException
- Name
statusCode- Type
- int
- Description
The HTTP status code of the response.
getCode()returns it too.
- Name
type- Type
- ?string
- Description
The problem type URI.
- Name
title- Type
- ?string
- Description
A short summary of the problem type.
- Name
detail- Type
- ?string
- Description
The problem detail, or the body of a
text/plainerror.
- Name
errors- Type
- array<string, list<string>>
- Description
Validation errors by field, from a 422. Entries that are not lists of strings are dropped.
- Name
requestId- Type
- ?string
- Description
The
X-Request-Idheader, or the problem'sinstancewhen the header is absent. Quote it when contacting Synthient support.
- Name
retryAfter- Type
- ?int
- Description
Seconds from the
Retry-Afterheader, which may be a number of seconds or an HTTP date, ornullif the header is absent or unreadable. Past dates and negative values become0.
- Name
headers- Type
- string[][]
- Description
The response headers.
- Logging:
getMessage()holds only the status, such assynthient: 402 Payment Required, so it is safe to log. - Untrusted fields:
title,detail,errors, andheaderscarry untrusted, possibly sensitive server data, so inspect them explicitly. - Downloads: An error from the storage host of an export download sets only
statusCode.
The constructor is public, so tests can create an ApiException with named arguments.
public function __construct(
public readonly int $statusCode,
public readonly ?string $type = null,
public readonly ?string $title = null,
public readonly ?string $detail = null,
public readonly array $errors = [],
public readonly ?string $requestId = null,
public readonly ?int $retryAfter = null,
public readonly array $headers = [],
?\Throwable $previous = null,
)
Retries and timeouts
Which requests are retried:
- Account and feed requests are retried on 500, 502, 503, and 504 responses, on transport failures other than TLS errors, and on 429 responses that carry
Retry-After. They areaccount(),listExports(),allExports(),exportMeta(),downloadUrl(), the API request behinddownload(), and stream connections. - Billed lookups (
lookupIp(),lookupIps(), andlookupDomain()) andhealth()are retried only on 429 responses that carryRetry-After, which the API returns before doing any work, so a retried lookup is never charged twice. They are also never sent on a reused connection, because libcurl silently resends a request whose reused connection dies before the response starts. - A 429 without
Retry-Aftermeans the key's monthly request budget is spent or too many of its streams are open, so it is thrown without a retry. - A failure while reading a stream or download is never retried, and neither is a download's request to the export storage host.
How retries wait:
- Retries back off exponentially from one second, doubling up to 30 seconds, with 25% jitter.
- The wait is never shorter than
Retry-After. - A
Retry-Afterlonger than a minute is not waited for: theApiExceptionis thrown with itsretryAfterinstead. - Set the retry count with
maxRetries.
How timeouts work:
connectTimeoutbounds each connection attempt. Guzzle's stream handler, which serves streams, downloads, and every request when the cURL extension is missing, ignores it and waits up totimeoutfor the connection instead.timeoutbounds each attempt's wait for the whole response, or for the next data on a stream or download.- The API sends a stream keepalive every 15 seconds.
- A connection, a response, or a read that takes too long throws a
TimeoutException. A connection or response timeout counts as a transport failure under the retry rules above, so it is thrown only once no retry is left. - Retries get a fresh timeout, so a call with retries can take longer than
timeout.
use Synthient\Exception\TimeoutException;
$client = new Synthient\Client(connectTimeout: 2.0, timeout: 5.0);
try {
$info = $client->lookupIp('8.8.8.8');
echo $info->ip, "\n";
} catch (TimeoutException $e) {
echo $e->getMessage(), "\n";
}