Skip to content

feat: add specific exceptions on provider errors and add retry#103

Open
levivannoort wants to merge 1 commit into
mainfrom
CLO-4353-build-timeout-issue
Open

feat: add specific exceptions on provider errors and add retry#103
levivannoort wants to merge 1 commit into
mainfrom
CLO-4353-build-timeout-issue

Conversation

@levivannoort
Copy link
Copy Markdown

No description provided.

@levivannoort levivannoort marked this pull request as ready for review May 22, 2026 08:05
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR adds typed exceptions (ProviderRateLimited, ProviderServerError, ProviderRequestFailed) and wraps the call() method in an exponential-backoff retry loop for transient network errors, rate-limit responses, and 5xx server errors. The adapter-level callers (GitHub, GitLab, Gitea) are updated to distinguish 404 from other 4xx responses.

  • Adapter::call() now retries up to 3 times with 1s/2s/4s backoff for curl errors, 429/403-rate-limit, and 5xx; all other status codes are returned to callers immediately.
  • The three provider adapters now check for 404 explicitly before falling through to a generic 4xx exception, and strengthen response-body guards with is_array() checks.
  • Three minimal exception classes are introduced under Utopia\\VCS\\Exception\\ extending \\Exception.

Confidence Score: 3/5

The retry logic in Adapter.php has a sequencing defect that silently defeats retries on the most common real-world 5xx scenario.

The JSON decode block runs before the HTTP status code is inspected, so a 500/503 response carrying Content-Type: application/json with a non-JSON body throws ProviderRequestFailed immediately without entering the retry path.

src/VCS/Adapter.php — specifically the ordering of JSON decoding relative to the HTTP status checks inside the retry loop.

Important Files Changed

Filename Overview
src/VCS/Adapter.php Core retry logic added with exponential backoff; JSON decode runs before HTTP status check, which short-circuits the 5xx retry path on malformed responses. Two unused variables and one unreachable branch also present.
src/VCS/Adapter/Git/GitHub.php 404 is now checked explicitly before the generic missing-field guard; logic looks correct.
src/VCS/Adapter/Git/GitLab.php 404 split from generic 4xx, missing-field guard strengthened with is_array check; looks correct.
src/VCS/Adapter/Git/Gitea.php Same 404/4xx split pattern applied consistently; looks correct.
src/VCS/Exception/ProviderRateLimited.php New exception class, minimal and correct.
src/VCS/Exception/ProviderRequestFailed.php New exception class, minimal and correct.
src/VCS/Exception/ProviderServerError.php New exception class, minimal and correct.

Reviews (1): Last reviewed commit: "feat: add specific exceptions on provide..." | Re-trigger Greptile

Comment thread src/VCS/Adapter.php
Comment on lines +419 to +432
if ($decode) {
$length = strpos($responseType, ';') ?: strlen($responseType);
switch (substr($responseType, 0, $length)) {
case 'application/json':
$json = \json_decode($responseBody, true);

$responseType = $responseHeaders['content-type'] ?? '';
$responseStatus = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($json === null) {
throw new ProviderRequestFailed('Failed to parse response: ' . $responseBody, $responseStatus);
}

if ($decode) {
$length = strpos($responseType, ';') ?: strlen($responseType);
switch (substr($responseType, 0, $length)) {
case 'application/json':
$json = \json_decode($responseBody, true);
$responseBody = $json;
$json = null;
break;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 JSON decode runs before HTTP status check, breaking 5xx retry

The JSON decoding block (lines 419–432) executes before the 5xx retry check (line 449). When a server returns a 500/503 with Content-Type: application/json but a non-JSON body (e.g., an HTML error page from a proxy/load balancer), json_decode returns null and ProviderRequestFailed is thrown immediately at line 426 — skipping all retry attempts for that request. The status code check for retryable errors should happen before decoding so that the retry path isn't short-circuited.

Comment thread src/VCS/Adapter.php
Comment on lines +352 to +355
$lastException = null;
$lastResponseStatus = 0;
$lastResponseBody = '';
$lastResponseHeaders = [];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused variables $lastResponseBody and $lastResponseHeaders

Both variables are assigned inside the retry loop but are never read anywhere in the method. They can be removed.

Suggested change
$lastException = null;
$lastResponseStatus = 0;
$lastResponseBody = '';
$lastResponseHeaders = [];
$lastException = null;
$lastResponseStatus = 0;

Comment thread src/VCS/Adapter.php
Comment on lines +396 to +402
$responseBody = \curl_exec($ch) ?: '';

if ($method != self::METHOD_GET) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
}
if ($responseBody === true) {
$responseBody = '';
}

// Allow self signed certificates
if ($this->selfSigned) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
}
$curlErrno = curl_errno($ch);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 $responseBody === true is unreachable

curl_exec() with CURLOPT_RETURNTRANSFER=1 returns a string or false; the ?: '' operator already converts false to '', so $responseBody is always a string at this point and can never equal true. The check can be removed.

Suggested change
$responseBody = \curl_exec($ch) ?: '';
if ($method != self::METHOD_GET) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
}
if ($responseBody === true) {
$responseBody = '';
}
// Allow self signed certificates
if ($this->selfSigned) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
}
$curlErrno = curl_errno($ch);
$responseBody = \curl_exec($ch) ?: '';
$curlErrno = curl_errno($ch);

Comment thread src/VCS/Adapter.php
Comment on lines +448 to +453
// Server errors (5xx) — retry
if ($responseStatus >= 500) {
$lastResponseStatus = $responseStatus;
$lastResponseBody = $responseBody;
$lastResponseHeaders = $responseHeaders;
if ($attempt < $this->maxRetries) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Remove the two corresponding in-loop assignments of the unused variables

Suggested change
// Server errors (5xx) — retry
if ($responseStatus >= 500) {
$lastResponseStatus = $responseStatus;
$lastResponseBody = $responseBody;
$lastResponseHeaders = $responseHeaders;
if ($attempt < $this->maxRetries) {
// Server errors (5xx) — retry
if ($responseStatus >= 500) {
$lastResponseStatus = $responseStatus;
if ($attempt < $this->maxRetries) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants