99use Fetch \Interfaces \Response as ResponseInterface ;
1010use Fetch \Support \RetryStrategy ;
1111use GuzzleHttp \Exception \ConnectException ;
12- use InvalidArgumentException ;
1312use Psr \Log \NullLogger ;
14- use RuntimeException ;
15- use Throwable ;
1613
1714trait ManagesRetries
1815{
@@ -40,7 +37,7 @@ trait ManagesRetries
4037 /**
4138 * The exceptions that should be retried.
4239 *
43- * @var array<class-string<Throwable>>
40+ * @var array<class-string<\ Throwable>>
4441 */
4542 protected array $ retryableExceptions = [
4643 ConnectException::class,
@@ -49,20 +46,21 @@ trait ManagesRetries
4946 /**
5047 * Set the retry logic for the request.
5148 *
52- * @param int $retries Maximum number of retry attempts
53- * @param int $delay Initial delay in milliseconds
49+ * @param int $retries Maximum number of retry attempts
50+ * @param int $delay Initial delay in milliseconds
51+ *
5452 * @return $this
5553 *
56- * @throws InvalidArgumentException If the parameters are invalid
54+ * @throws \ InvalidArgumentException If the parameters are invalid
5755 */
5856 public function retry (int $ retries , int $ delay = 100 ): ClientHandler
5957 {
6058 if ($ retries < 0 ) {
61- throw new InvalidArgumentException ('Retries must be a non-negative integer ' );
59+ throw new \ InvalidArgumentException ('Retries must be a non-negative integer ' );
6260 }
6361
6462 if ($ delay < 0 ) {
65- throw new InvalidArgumentException ('Delay must be a non-negative integer ' );
63+ throw new \ InvalidArgumentException ('Delay must be a non-negative integer ' );
6664 }
6765
6866 $ this ->maxRetries = $ retries ;
@@ -74,7 +72,8 @@ public function retry(int $retries, int $delay = 100): ClientHandler
7472 /**
7573 * Set the status codes that should be retried.
7674 *
77- * @param array<int> $statusCodes HTTP status codes
75+ * @param array<int> $statusCodes HTTP status codes
76+ *
7877 * @return $this
7978 */
8079 public function retryStatusCodes (array $ statusCodes ): ClientHandler
@@ -87,7 +86,8 @@ public function retryStatusCodes(array $statusCodes): ClientHandler
8786 /**
8887 * Set the exception types that should be retried.
8988 *
90- * @param array<class-string<Throwable>> $exceptions Exception class names
89+ * @param array<class-string<\Throwable>> $exceptions Exception class names
90+ *
9191 * @return $this
9292 */
9393 public function retryExceptions (array $ exceptions ): ClientHandler
@@ -130,7 +130,7 @@ public function getRetryableStatusCodes(): array
130130 /**
131131 * Get the retryable exception types.
132132 *
133- * @return array<class-string<Throwable>> The retryable exception classes
133+ * @return array<class-string<\ Throwable>> The retryable exception classes
134134 */
135135 public function getRetryableExceptions (): array
136136 {
@@ -140,45 +140,77 @@ public function getRetryableExceptions(): array
140140 /**
141141 * Implement retry logic for the request with exponential backoff.
142142 *
143- * This method now accepts an optional RequestContext to read retry configuration
143+ * This method accepts an optional RequestContext to read retry configuration
144144 * from per-request context instead of handler state, making it safe for concurrent usage.
145+ * All retry settings (maxRetries, retryDelay, retryableStatusCodes, retryableExceptions)
146+ * are read from the context when provided, falling back to handler state otherwise.
147+ *
148+ * @param \Fetch\Support\RequestContext|null $context The request context (optional)
149+ * @param callable $request The request to execute
145150 *
146- * @param \Fetch\Support\RequestContext|null $context The request context (optional)
147- * @param callable $request The request to execute
148151 * @return ResponseInterface The response after successful execution
149152 *
150153 * @throws FetchRequestException If the request fails after all retries
151- * @throws RuntimeException If something unexpected happens
154+ * @throws \ RuntimeException If something unexpected happens
152155 */
153156 protected function retryRequest (?\Fetch \Support \RequestContext $ context , callable $ request ): ResponseInterface
154157 {
155158 // Read retry config from context if provided, otherwise fall back to handler state
156159 $ maxRetries = $ context ?->getMaxRetries() ?? $ this ->getMaxRetries ();
157160 $ baseDelayMs = $ context ?->getRetryDelay() ?? $ this ->getRetryDelay ();
158161
162+ // Read retryable status codes and exceptions from context, falling back to handler state
163+ $ retryableStatusCodes = null !== $ context
164+ ? $ context ->getRetryableStatusCodes ()
165+ : $ this ->retryableStatusCodes ;
166+
167+ $ retryableExceptions = null !== $ context
168+ ? $ context ->getRetryableExceptions ()
169+ : $ this ->retryableExceptions ;
170+
159171 $ strategy = new RetryStrategy (
160172 maxRetries: $ maxRetries ,
161173 baseDelayMs: $ baseDelayMs ,
162- retryableStatusCodes: $ this -> retryableStatusCodes ,
163- retryableExceptions: $ this -> retryableExceptions ,
164- logger: $ this ->logger ?? new NullLogger
174+ retryableStatusCodes: $ retryableStatusCodes ,
175+ retryableExceptions: $ retryableExceptions ,
176+ logger: $ this ->logger ?? new NullLogger ()
165177 );
166178
167179 return $ strategy ->execute (
168180 $ request ,
169- function (int $ attempt , int $ maxAttempts , Throwable $ exception , int $ delayMs ): void {
170- if (method_exists ($ this , 'logRetry ' )) {
171- $ this ->logRetry ($ attempt , $ maxAttempts , $ exception );
172- }
181+ function (int $ attempt , int $ maxAttempts , \Throwable $ exception , int $ delayMs ) use ($ context ): void {
182+ $ this ->logRetryAttempt ($ attempt , $ maxAttempts , $ exception , $ delayMs , $ context );
173183 }
174184 );
175185 }
176186
187+ /**
188+ * Log a retry attempt with context information.
189+ *
190+ * @param int $attempt Current attempt number
191+ * @param int $maxAttempts Maximum number of attempts
192+ * @param \Throwable $exception The exception that triggered the retry
193+ * @param int $delayMs The delay before the next attempt in milliseconds
194+ * @param \Fetch\Support\RequestContext|null $context Optional request context for additional info
195+ */
196+ protected function logRetryAttempt (
197+ int $ attempt ,
198+ int $ maxAttempts ,
199+ \Throwable $ exception ,
200+ int $ delayMs ,
201+ ?\Fetch \Support \RequestContext $ context = null ,
202+ ): void {
203+ if (method_exists ($ this , 'logRetry ' )) {
204+ $ this ->logRetry ($ attempt , $ maxAttempts , $ exception );
205+ }
206+ }
207+
177208 /**
178209 * Calculate backoff delay with exponential growth and jitter.
179210 *
180- * @param int $baseDelay The base delay in milliseconds
181- * @param int $attempt The current attempt number (0-based)
211+ * @param int $baseDelay The base delay in milliseconds
212+ * @param int $attempt The current attempt number (0-based)
213+ *
182214 * @return int The calculated delay in milliseconds
183215 */
184216 protected function calculateBackoffDelay (int $ baseDelay , int $ attempt ): int
@@ -197,11 +229,22 @@ protected function calculateBackoffDelay(int $baseDelay, int $attempt): int
197229 /**
198230 * Determine if an error is retryable.
199231 *
200- * @param Throwable $e The exception to check
232+ * @param \Throwable $e The exception to check
233+ * @param \Fetch\Support\RequestContext|null $context Optional context for per-request retry config
234+ *
201235 * @return bool Whether the error is retryable
202236 */
203- protected function isRetryableError (Throwable $ e ): bool
237+ protected function isRetryableError (\ Throwable $ e, ? \ Fetch \ Support \ RequestContext $ context = null ): bool
204238 {
239+ // Use context values if provided, otherwise fall back to handler state
240+ $ retryableStatusCodes = null !== $ context
241+ ? $ context ->getRetryableStatusCodes ()
242+ : $ this ->retryableStatusCodes ;
243+
244+ $ retryableExceptions = null !== $ context
245+ ? $ context ->getRetryableExceptions ()
246+ : $ this ->retryableExceptions ;
247+
205248 $ statusCode = null ;
206249
207250 // Try to extract status code from a Fetch RequestException
@@ -217,14 +260,14 @@ protected function isRetryableError(Throwable $e): bool
217260 }
218261
219262 // Check if the status code is in our list of retryable codes
220- $ isRetryableStatusCode = $ statusCode !== null && in_array ($ statusCode , $ this -> retryableStatusCodes , true );
263+ $ isRetryableStatusCode = null !== $ statusCode && in_array ($ statusCode , $ retryableStatusCodes , true );
221264
222265 // Check if the exception or its previous is one of our retryable exception types
223266 $ isRetryableException = false ;
224267 $ exception = $ e ;
225268
226269 while ($ exception ) {
227- if (in_array (get_class ($ exception ), $ this -> retryableExceptions , true )) {
270+ if (in_array (get_class ($ exception ), $ retryableExceptions , true )) {
228271 $ isRetryableException = true ;
229272 break ;
230273 }
0 commit comments