Skip to content

Commit d6af7e8

Browse files
committed
feat: Add default configuration and retry handling for Fetch HTTP client
- Introduced centralized default values in `Defaults.php` for HTTP method, timeout, retries, and retry delay. - Created `RetryDefaults.php` to manage retry-related configurations, including maximum retries, delay, and retryable status codes. - Refactored `ClientHandler.php`, `GlobalServices.php`, and `RequestContext.php` to utilize new defaults. - Implemented `RetryStrategy.php` to encapsulate retry logic and reduce duplication. - Updated `RequestOptions.php` to normalize multipart requests. - Enhanced `ManagesRetries.php` and `PerformsHttpRequests.php` to leverage new retry defaults and strategies. - Added tests in `AsyncRequestsTest.php` to ensure proper handling of async requests and rejection scenarios. - Created configuration files for Obsidian app settings and workspace layout.
1 parent cab4397 commit d6af7e8

File tree

13 files changed

+279
-384
lines changed

13 files changed

+279
-384
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ notes.md
3434
docs/.vitepress/dist
3535
docs/.vitepress/cache
3636
/duster_*
37+
docs/.obsidian/*

phpunit.xml.dist

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,4 @@
3535
<directory>src/Fetch/Support</directory>
3636
</exclude>
3737
</source>
38-
<coverage includeUncoveredFiles="true">
39-
<report>
40-
<xml outputDirectory="coverage"/>
41-
<html outputDirectory="coverage" lowUpperBound="50" highLowerBound="80"/>
42-
<text outputFile="php://stdout" showUncoveredFiles="false" showOnlySummary="false"/>
43-
</report>
44-
</coverage>
4538
</phpunit>

src/Fetch/Concerns/ConfiguresRequests.php

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -447,22 +447,6 @@ protected function unsetConflictingOptions(array $keys): void
447447
*/
448448
protected function normalizeMultipart(array $multipart): array
449449
{
450-
if ($multipart === [] || array_is_list($multipart)) {
451-
/** @var array<int, array{name: string, contents: mixed, headers?: array<string, string>}> $multipart */
452-
return $multipart;
453-
}
454-
455-
$part = [
456-
'name' => (string) ($multipart['name'] ?? 'file'),
457-
'contents' => $multipart['contents'] ?? ($multipart['body'] ?? ''),
458-
];
459-
460-
if (isset($multipart['headers']) && is_array($multipart['headers'])) {
461-
$part['headers'] = array_map(static function ($v): string {
462-
return (string) $v;
463-
}, $multipart['headers']);
464-
}
465-
466-
return [$part];
450+
return RequestOptions::normalizeMultipart($multipart);
467451
}
468452
}

src/Fetch/Concerns/ManagesRetries.php

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
use Fetch\Exceptions\RequestException as FetchRequestException;
88
use Fetch\Interfaces\ClientHandler;
99
use Fetch\Interfaces\Response as ResponseInterface;
10+
use Fetch\Support\RetryDefaults;
1011
use Fetch\Support\RetryStrategy;
11-
use GuzzleHttp\Exception\ConnectException;
1212
use InvalidArgumentException;
1313
use Psr\Log\NullLogger;
1414

@@ -29,20 +29,14 @@ trait ManagesRetries
2929
*
3030
* @var array<int>
3131
*/
32-
protected array $retryableStatusCodes = [
33-
408, 429, 500, 502, 503,
34-
504, 507, 509, 520, 521,
35-
522, 523, 525, 527, 530,
36-
];
32+
protected array $retryableStatusCodes = RetryDefaults::STATUS_CODES;
3733

3834
/**
3935
* The exceptions that should be retried.
4036
*
4137
* @var array<class-string<\Throwable>>
4238
*/
43-
protected array $retryableExceptions = [
44-
ConnectException::class,
45-
];
39+
protected array $retryableExceptions = RetryDefaults::EXCEPTIONS;
4640

4741
/**
4842
* Set the retry logic for the request.
@@ -205,26 +199,24 @@ protected function logRetryAttempt(
205199
/**
206200
* Calculate backoff delay with exponential growth and jitter.
207201
*
202+
* Delegates to RetryStrategy for the actual calculation to avoid duplication.
203+
*
208204
* @param int $baseDelay The base delay in milliseconds
209205
* @param int $attempt The current attempt number (0-based)
210206
* @return int The calculated delay in milliseconds
211207
*/
212208
protected function calculateBackoffDelay(int $baseDelay, int $attempt): int
213209
{
214-
// Exponential backoff: baseDelay * 2^attempt
215-
$exponentialDelay = $baseDelay * (2 ** $attempt);
216-
217-
// Add jitter: random value between 0-100% of the calculated delay
218-
$jitter = mt_rand(0, 100) / 100; // Random value between 0 and 1
219-
$delay = (int) ($exponentialDelay * (1 + $jitter));
210+
$strategy = new RetryStrategy(baseDelayMs: $baseDelay);
220211

221-
// Cap the maximum delay at 30 seconds (30000ms)
222-
return min($delay, 30000);
212+
return $strategy->calculateDelay($attempt);
223213
}
224214

225215
/**
226216
* Determine if an error is retryable.
227217
*
218+
* Delegates to RetryStrategy for the actual check to avoid duplication.
219+
*
228220
* @param \Throwable $e The exception to check
229221
* @param \Fetch\Support\RequestContext|null $context Optional context for per-request retry config
230222
* @return bool Whether the error is retryable
@@ -240,35 +232,11 @@ protected function isRetryableError(\Throwable $e, ?\Fetch\Support\RequestContex
240232
? $context->getRetryableExceptions()
241233
: $this->retryableExceptions;
242234

243-
$statusCode = null;
244-
245-
// Try to extract status code from a Fetch RequestException
246-
if ($e instanceof FetchRequestException && $e->getResponse()) {
247-
$statusCode = $e->getResponse()->getStatusCode();
248-
} elseif (method_exists($e, 'getResponse')) {
249-
// Guzzle RequestException also has getResponse()
250-
$response = $e->getResponse();
251-
if ($response && method_exists($response, 'getStatusCode')) {
252-
/** @var \Psr\Http\Message\ResponseInterface $response */
253-
$statusCode = $response->getStatusCode();
254-
}
255-
}
256-
257-
// Check if the status code is in our list of retryable codes
258-
$isRetryableStatusCode = $statusCode !== null && in_array($statusCode, $retryableStatusCodes, true);
259-
260-
// Check if the exception or its previous is one of our retryable exception types
261-
$isRetryableException = false;
262-
$exception = $e;
263-
264-
while ($exception) {
265-
if (in_array(get_class($exception), $retryableExceptions, true)) {
266-
$isRetryableException = true;
267-
break;
268-
}
269-
$exception = $exception->getPrevious();
270-
}
235+
$strategy = new RetryStrategy(
236+
retryableStatusCodes: $retryableStatusCodes,
237+
retryableExceptions: $retryableExceptions,
238+
);
271239

272-
return $isRetryableStatusCode || $isRetryableException;
240+
return $strategy->isRetryable($e);
273241
}
274242
}

src/Fetch/Concerns/PerformsHttpRequests.php

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -747,41 +747,14 @@ private function applyFormParamsPure(array $options, array $body): array
747747
private function applyMultipartPure(array $options, array $body): array
748748
{
749749
// Normalize multipart if needed
750-
$multipart = $this->normalizeMultipartPure($body);
750+
$multipart = RequestOptions::normalizeMultipart($body);
751751
$options['multipart'] = $multipart;
752752
// Remove Content-Type header as Guzzle sets it with boundary
753753
unset($options['headers']['Content-Type']);
754754

755755
return $options;
756756
}
757757

758-
/**
759-
* Normalize multipart array without mutation.
760-
*
761-
* @param array<int, array{name: string, contents: mixed, headers?: array<string, string>}>|array<string, mixed> $multipart
762-
* @return array<int, array{name: string, contents: mixed, headers?: array<string, string>}>
763-
*/
764-
private function normalizeMultipartPure(array $multipart): array
765-
{
766-
if ($multipart === [] || array_is_list($multipart)) {
767-
/* @var array<int, array{name: string, contents: mixed, headers?: array<string, string>}> $multipart */
768-
return $multipart;
769-
}
770-
771-
$part = [
772-
'name' => (string) ($multipart['name'] ?? 'file'),
773-
'contents' => $multipart['contents'] ?? ($multipart['body'] ?? ''),
774-
];
775-
776-
if (isset($multipart['headers']) && is_array($multipart['headers'])) {
777-
$part['headers'] = array_map(static function ($v): string {
778-
return (string) $v;
779-
}, $multipart['headers']);
780-
}
781-
782-
return [$part];
783-
}
784-
785758
/**
786759
* Apply generic array body as JSON string without mutation.
787760
*

src/Fetch/Http/ClientHandler.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
use Fetch\Enum\Method;
1919
use Fetch\Interfaces\ClientHandler as ClientHandlerInterface;
2020
use Fetch\Interfaces\Response as ResponseContract;
21+
use Fetch\Support\Defaults;
2122
use Fetch\Support\GlobalServices;
2223
use Fetch\Support\RequestContext;
2324
use Fetch\Support\RequestOptions as FetchRequestOptions;
25+
use Fetch\Support\RetryDefaults;
2426
use GuzzleHttp\Client as GuzzleClient;
2527
use GuzzleHttp\ClientInterface;
2628
use GuzzleHttp\RequestOptions;
@@ -49,29 +51,37 @@ class ClientHandler implements ClientHandlerInterface
4951
* @deprecated Use GlobalServices::getDefaultOptions() instead
5052
*/
5153
protected static array $defaultOptions = [
52-
'method' => self::DEFAULT_HTTP_METHOD,
54+
'method' => Defaults::HTTP_METHOD->value,
5355
'headers' => [],
5456
];
5557

5658
/**
5759
* Default HTTP method.
60+
*
61+
* @deprecated Use Defaults::HTTP_METHOD instead
5862
*/
59-
public const DEFAULT_HTTP_METHOD = 'GET';
63+
public const DEFAULT_HTTP_METHOD = Defaults::HTTP_METHOD->value;
6064

6165
/**
6266
* Default timeout for requests in seconds.
67+
*
68+
* @deprecated Use Defaults::TIMEOUT instead
6369
*/
64-
public const DEFAULT_TIMEOUT = 30;
70+
public const DEFAULT_TIMEOUT = Defaults::TIMEOUT;
6571

6672
/**
6773
* Default number of retries.
74+
*
75+
* @deprecated Use RetryDefaults::MAX_RETRIES instead
6876
*/
69-
public const DEFAULT_RETRIES = 1;
77+
public const DEFAULT_RETRIES = RetryDefaults::MAX_RETRIES;
7078

7179
/**
7280
* Default delay between retries in milliseconds.
81+
*
82+
* @deprecated Use RetryDefaults::RETRY_DELAY instead
7383
*/
74-
public const DEFAULT_RETRY_DELAY = 100;
84+
public const DEFAULT_RETRY_DELAY = RetryDefaults::RETRY_DELAY;
7585

7686
/**
7787
* Whether the request should be asynchronous.

src/Fetch/Support/Defaults.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fetch\Support;
6+
7+
use Fetch\Enum\Method;
8+
9+
/**
10+
* Centralized defaults for all Fetch HTTP client configuration.
11+
*
12+
* This class serves as the single source of truth for all default values,
13+
* eliminating duplication across ClientHandler, GlobalServices, and other classes.
14+
*
15+
* @internal
16+
*/
17+
final class Defaults
18+
{
19+
/**
20+
* Default HTTP method for requests.
21+
*/
22+
public const HTTP_METHOD = Method::GET;
23+
24+
/**
25+
* Default timeout for requests in seconds.
26+
*/
27+
public const TIMEOUT = 30;
28+
29+
/**
30+
* Default number of retry attempts.
31+
*
32+
* @deprecated Use RetryDefaults::MAX_RETRIES instead
33+
*/
34+
public const RETRIES = RetryDefaults::MAX_RETRIES;
35+
36+
/**
37+
* Default delay between retries in milliseconds.
38+
*
39+
* @deprecated Use RetryDefaults::RETRY_DELAY instead
40+
*/
41+
public const RETRY_DELAY = RetryDefaults::RETRY_DELAY;
42+
43+
/**
44+
* Prevent instantiation of this utility class.
45+
*/
46+
private function __construct() {}
47+
48+
/**
49+
* Get the factory default options array.
50+
*
51+
* @return array<string, mixed>
52+
*/
53+
public static function options(): array
54+
{
55+
return [
56+
'method' => self::HTTP_METHOD->value,
57+
'timeout' => self::TIMEOUT,
58+
'headers' => [],
59+
];
60+
}
61+
}

src/Fetch/Support/GlobalServices.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,7 @@ public static function initialize(
9898
*/
9999
public static function getFactoryDefaults(): array
100100
{
101-
return [
102-
'method' => Method::GET->value,
103-
'headers' => [],
104-
'timeout' => 30, // DEFAULT_TIMEOUT constant value
105-
];
101+
return Defaults::options();
106102
}
107103

108104
/**

0 commit comments

Comments
 (0)