-
-
Notifications
You must be signed in to change notification settings - Fork 26
Add Request/Response Hooks & Events System #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,11 +4,14 @@ | |
|
|
||
| namespace Fetch\Concerns; | ||
|
|
||
| use Fetch\Events\ErrorEvent; | ||
| use Fetch\Events\RetryEvent; | ||
| use Fetch\Exceptions\RequestException as FetchRequestException; | ||
| use Fetch\Interfaces\ClientHandler; | ||
| use Fetch\Interfaces\Response as ResponseInterface; | ||
| use GuzzleHttp\Exception\ConnectException; | ||
| use InvalidArgumentException; | ||
| use Psr\Http\Message\RequestInterface; | ||
| use RuntimeException; | ||
| use Throwable; | ||
|
|
||
|
|
@@ -261,4 +264,119 @@ protected function isRetryableError(Throwable $e): bool | |
|
|
||
| return $isRetryableStatusCode || $isRetryableException; | ||
| } | ||
|
|
||
| /** | ||
| * Implement retry logic for the request with event dispatching. | ||
| * | ||
| * @param callable $request The request to execute | ||
| * @param RequestInterface $psrRequest The PSR-7 request for events | ||
| * @param string $correlationId The correlation ID for event tracking | ||
| * @return ResponseInterface The response after successful execution | ||
| * | ||
| * @throws FetchRequestException If the request fails after all retries | ||
| * @throws RuntimeException If something unexpected happens | ||
| */ | ||
| protected function retryRequestWithEvents( | ||
| callable $request, | ||
| RequestInterface $psrRequest, | ||
| string $correlationId | ||
| ): ResponseInterface { | ||
| $attempts = $this->maxRetries ?? self::DEFAULT_RETRIES; | ||
| $delay = $this->retryDelay ?? self::DEFAULT_RETRY_DELAY; | ||
| $exceptions = []; | ||
|
|
||
| for ($attempt = 0; $attempt <= $attempts; $attempt++) { | ||
| try { | ||
| // Execute the request | ||
| return $request(); | ||
| } catch (Throwable $e) { | ||
| // Collect exception for later | ||
| $exceptions[] = $e; | ||
|
|
||
| // Get response from exception if available | ||
| $response = null; | ||
| if ($e instanceof FetchRequestException && $e->getResponse()) { | ||
| $response = $e->getResponse(); | ||
| } elseif (method_exists($e, 'getResponse')) { | ||
| $response = $e->getResponse(); | ||
| } | ||
|
|
||
| // If this was the last attempt, dispatch error event and break | ||
| if ($attempt === $attempts) { | ||
| // Dispatch error event | ||
| if (method_exists($this, 'dispatchEvent')) { | ||
| $this->dispatchEvent(new ErrorEvent( | ||
| $psrRequest, | ||
| $e, | ||
| $correlationId, | ||
| microtime(true), | ||
| $attempt + 1, | ||
| $response | ||
| )); | ||
| } | ||
| break; | ||
| } | ||
|
|
||
| // Only retry on retryable errors | ||
| if (! $this->isRetryableError($e)) { | ||
| // Dispatch error event for non-retryable errors | ||
| if (method_exists($this, 'dispatchEvent')) { | ||
| $this->dispatchEvent(new ErrorEvent( | ||
| $psrRequest, | ||
| $e, | ||
| $correlationId, | ||
| microtime(true), | ||
| $attempt + 1, | ||
| $response | ||
| )); | ||
| } | ||
| throw $e; | ||
| } | ||
|
|
||
| // Log the retry for debugging purposes | ||
| if (method_exists($this, 'logRetry')) { | ||
| $this->logRetry($attempt + 1, $attempts, $e); | ||
| } | ||
|
|
||
| // Calculate delay with exponential backoff and jitter | ||
| $currentDelay = $this->calculateBackoffDelay($delay, $attempt); | ||
|
|
||
| // Dispatch retry event | ||
| if (method_exists($this, 'dispatchEvent')) { | ||
| $this->dispatchEvent(new RetryEvent( | ||
| $psrRequest, | ||
| $e, | ||
| $attempt + 1, | ||
| $attempts, | ||
| $currentDelay, | ||
| $correlationId, | ||
| microtime(true) | ||
| )); | ||
| } | ||
|
Comment on lines
+288
to
+355
|
||
|
|
||
| // Sleep before the next retry | ||
| usleep($currentDelay * 1000); // Convert milliseconds to microseconds | ||
| } | ||
| } | ||
|
|
||
| // If we got here, all retries failed | ||
| $lastException = end($exceptions) ?: new RuntimeException('Request failed after all retries'); | ||
|
|
||
| // Enhanced failure reporting | ||
| if ($lastException instanceof FetchRequestException && $lastException->getResponse()) { | ||
| $statusCode = $lastException->getResponse()->getStatusCode(); | ||
| throw new RuntimeException( | ||
| sprintf( | ||
| 'Request failed after %d attempts with status code %d: %s', | ||
| $attempts + 1, | ||
| $statusCode, | ||
| $lastException->getMessage() | ||
| ), | ||
| $statusCode, | ||
| $lastException | ||
| ); | ||
| } | ||
|
Comment on lines
+366
to
+378
|
||
|
|
||
| throw $lastException; | ||
| } | ||
|
Comment on lines
+279
to
+381
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,8 @@ | |||||||||
|
|
||||||||||
| use Fetch\Enum\ContentType; | ||||||||||
| use Fetch\Enum\Method; | ||||||||||
| use Fetch\Events\RequestEvent; | ||||||||||
| use Fetch\Events\ResponseEvent; | ||||||||||
| use Fetch\Exceptions\RequestException as FetchRequestException; | ||||||||||
| use Fetch\Http\Response; | ||||||||||
| use Fetch\Interfaces\Response as ResponseInterface; | ||||||||||
|
|
@@ -182,16 +184,33 @@ public function sendRequest( | |||||||||
| // Start timing for logging | ||||||||||
| $startTime = microtime(true); | ||||||||||
|
|
||||||||||
| // Generate a correlation ID for event tracking | ||||||||||
| $correlationId = method_exists($handler, 'generateCorrelationId') | ||||||||||
| ? $handler->generateCorrelationId() | ||||||||||
| : bin2hex(random_bytes(16)); | ||||||||||
|
Comment on lines
+188
to
+190
|
||||||||||
| $correlationId = method_exists($handler, 'generateCorrelationId') | |
| ? $handler->generateCorrelationId() | |
| : bin2hex(random_bytes(16)); | |
| $correlationId = $handler->generateCorrelationId(); |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The PSR-7 Request object is created twice in the same method:
- Line 199 for dispatching the RequestEvent
- Line 381 (in the original code) and line 426 (in the retry logic) for retry handling
Creating the PSR request once at line 381 and reusing it at line 199 would be more efficient and ensure both the event and the retry logic use the exact same request object. Currently, the request object used in events might have different headers than the one used in retry logic if headers are modified between these points.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop condition
for ($attempt = 0; $attempt <= $attempts; $attempt++)creates a potential off-by-one error. When$attempts = 3, the loop runs for$attempt = 0, 1, 2, 3, which is 4 iterations. This means:$attempts = 3should mean "3 retries" (4 total attempts including the initial)The variable naming is ambiguous. Either:
$attemptsto$maxAttemptsor$maxRetriesto clarify what it represents$attempt < $attemptsif$attemptsmeans total attemptsmaxRetriesmeans "number of retries after the initial attempt" or "total number of attempts"This same issue exists in the original
retryRequestmethod at line 156.