fix(s3): retry refused connections and unresolvable hosts - #170
fix(s3): retry refused connections and unresolvable hosts#170HarshMN2345 wants to merge 1 commit into
Conversation
The strategy returned null whenever there was no response, so it only retried failures the service had answered. A refused connection or an unresolvable host was passed straight through, even though the request never reached the service and replaying it cannot duplicate an effect. Seen in production: a Spaces endpoint refused connections for ~35 minutes and every upload failed on the first attempt with CURLE_COULDNT_CONNECT. Other transport failures stay non-retryable, since a reset or a timeout may have been applied server-side.
|
Thanks for contributing! This repository is a read-only mirror; development for this library happens in |
Greptile SummaryThe PR extends the S3 retry strategy to retry DNS and connection exceptions when no HTTP response exists.
Confidence Score: 4/5The PR should not merge until ConnectionException retries are restricted to failures that guarantee the request was not delivered. The new broad ConnectionException branch retries send and receive failures that may occur after a mutating S3 request has already been applied, allowing operations such as multipart-upload creation to execute twice. Files Needing Attention: src/Storage/Device/S3/RetryStrategy.php, tests/Storage/Device/S3/RetryStrategyTest.php Important Files Changed
Prompt To Fix All With AI### Issue 1
src/Storage/Device/S3/RetryStrategy.php:68
**Retries include delivered requests**
When a mutating S3 request fails during sending or response reception, the client reports several such failures as `ConnectionException`, and this branch retries the request even though the service may already have applied it. Replaying a non-idempotent request such as multipart-upload creation can create a second orphaned upload with associated storage costs.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "fix(s3): retry refused connections and u..." | Re-trigger Greptile |
|
|
||
| if (! $this->isTransient($response)) { | ||
| $retryable = $response instanceof ResponseInterface | ||
| ? $this->isTransient($response) |
There was a problem hiding this comment.
Retries include delivered requests
When a mutating S3 request fails during sending or response reception, the client reports several such failures as ConnectionException, and this branch retries the request even though the service may already have applied it. Replaying a non-idempotent request such as multipart-upload creation can create a second orphaned upload with associated storage costs.
Knowledge Base Used: S3 transport resilience
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Storage/Device/S3/RetryStrategy.php
Line: 68
Comment:
**Retries include delivered requests**
When a mutating S3 request fails during sending or response reception, the client reports several such failures as `ConnectionException`, and this branch retries the request even though the service may already have applied it. Replaying a non-idempotent request such as multipart-upload creation can create a second orphaned upload with associated storage costs.
**Knowledge Base Used:** [S3 transport resilience](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/storage/-/docs/s3-transport-resilience.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Problem
S3\RetryStrategy::delay()opened with:The second clause declines whenever there is no response, so the strategy only retried failures the service had answered — 429/503,
SlowDown,Throttling. A refused connection or an unresolvable host went straight through on the first attempt, even though theRetrydecorator already catchesClientExceptionInterfaceand callsdelay($request, $attempt, null, $error). The$errorargument was simply never used.Production impact
A DigitalOcean Spaces endpoint refused connections for ~35 minutes. Every upload through it failed on the first attempt:
In our case the payloads had already been produced by earlier work, so each failure discarded work that had succeeded.
Change
Retry when the request never reached the service:
ConnectionException—CURLE_COULDNT_CONNECTand friendsDnsException—CURLE_COULDNT_RESOLVE_HOSTReplaying these cannot duplicate an effect, because nothing was delivered.
Deliberately unchanged:
NetworkException,TimeoutExceptionand other transport failures stay non-retryable — the request may have been applied server-side, so a retry could duplicate it. The existingtestTransportErrorsAreNotRetried(which usesNetworkException) still passes unmodified.This does widen the strategy's scope beyond rate-limiting, so the class docblock is updated to say so.
Tests
Three added: refused connection retried, unresolvable host retried, and the retry cap honoured on the new path.
Verified against the real classes — the two new paths retry, and all existing behaviour is unchanged:
Note:
composer.jsonhas norequire-dev, so phpunit is not vendored and I could not run the suite through it locally — the above exercises the same assertions directly. Worth a CI run to confirm.🤖 Generated with Claude Code