shoxcie / batch-http-client
HTTP request batch executor with individual retries, built on Symfony HttpClient.
Requires
- php: ^8.4 || ^8.5
- symfony/http-client: ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- nunomaduro/pao: 0.1.5
- pestphp/pest: ^4.5
- pestphp/pest-plugin-type-coverage: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-strict-rules: ^2.0
- rector/rector: ^2.4
This package is auto-updated.
Last update: 2026-04-29 20:54:46 UTC
README
HTTP request batch executor with individual retries, built on Symfony HttpClient.
Fire multiple HTTP requests in parallel. Each request retries independently without blocking others.
Requirements
- PHP 8.4+
- Symfony HttpClient 8.0+
Installation
composer require shoxcie/batch-http-client
Usage
use Shoxcie\BatchHttpClient\BatchHttpClient; use Shoxcie\BatchHttpClient\RequestConfig; $client = new BatchHttpClient(); $results = $client ->request([ 'users' => new RequestConfig('GET', 'https://api.example.com/users'), 'orders' => new RequestConfig('POST', 'https://api.example.com/orders', options: [ 'json' => ['item' => 'widget', 'qty' => 3], ]), ]) ->fetch(); $results['users']; // decoded JSON array $results['orders']; // decoded JSON array
Retries
$results = $client ->request([ 'flaky' => new RequestConfig('GET', 'https://api.example.com/flaky', maxRetries: 3, ), ]) ->fetch();
Any non-2xx response or transport error (connection timeout, DNS failure) triggers a retry. Transport exception retries can be disabled per request with retryOnTransportException: false.
Retry options
Override options on retry with a static array:
new RequestConfig('GET', 'https://api.example.com/resource', maxRetries: 2, retryOptions: ['timeout' => 30], )
Or dynamically with a Closure:
new RequestConfig('GET', 'https://api.example.com/resource', maxRetries: 3, retryOptions: function (string $key, int $retries, ExceptionInterface|InvalidResponseException $e): array { return ['timeout' => 10 * $retries]; }, )
Retry options are merged onto the original options via array_replace_recursive().
Callbacks
$results = $client ->request([...]) ->onSuccess(function (string $key, int $retries, mixed $result, ResponseInterface $response) { // called for each 2xx response, after parseResponse if configured // $retries = 0 on first-attempt success, N on success after N retries }) ->onRetry(function (string $key, int $retries, ResponseInterface $failedResponse, ExceptionInterface|InvalidResponseException $e, ResponseInterface $retryResponse) { // called when a retry fires; $retries is the retry count after this retry fired (always >= 1) }) ->onExhausted(function (string $key, int $retries, ResponseInterface $response, ExceptionInterface|InvalidResponseException $e) { // called when a single request exhausts all retries // $retries equals maxRetries normally; less when a transport error short-circuits }) ->onAbort(function (string $key, int $retries, ResponseInterface $response, Throwable $e) { // called when an unexpected exception (e.g. throwing user callback) cancels the whole batch // $retries is the retry count for the request being processed when the abort fired }) ->fetch();
Error handling
By default, if a request exhausts all retries, the last exception is rethrown and all in-flight requests are cancelled:
new RequestConfig('GET', 'https://api.example.com/critical', maxRetries: 3, throwOnExhausted: true, // default )
Set throwOnExhausted: false for optional requests. Failed optional requests return null in the results array:
new RequestConfig('GET', 'https://api.example.com/optional', throwOnExhausted: false, )
Response decoding
By default, responses are decoded as JSON (toArray()). Set decodeJson: false to get raw content (getContent()):
new RequestConfig('GET', 'https://example.com/file.csv', decodeJson: false, )
Response parsing / validation
parseResponse runs after the body is decoded and before onSuccess, only on a 2xx response. The return value replaces the entry in the results array, so it doubles as a custom parser:
new RequestConfig('GET', 'https://api.example.com/users', parseResponse: fn(string $key, mixed $result, ResponseInterface $response): mixed => $result['data'], )
Throw InvalidResponseException from the parser to reject a semantically invalid 2xx response and trigger a retry (counts against maxRetries, fires onRetry, and on exhaustion fires onExhausted plus rethrows if throwOnExhausted: true):
use Shoxcie\BatchHttpClient\InvalidResponseException; new RequestConfig('GET', 'https://api.example.com/job-status', maxRetries: 5, parseResponse: function (string $key, mixed $result): mixed { if ($result['status'] === 'pending') { throw new InvalidResponseException('job not finished'); } return $result; }, )
Any other Throwable from the parser is treated as an unexpected error and routes to onAbort, cancelling the whole batch.
Custom HTTP client
Pass any HttpClientInterface implementation:
use Symfony\Component\HttpClient\HttpClient; $httpClient = HttpClient::create([ 'timeout' => 10, 'max_duration' => 30, ]); $client = new BatchHttpClient($httpClient);
RequestConfig reference
| Parameter | Type | Default | Description |
|---|---|---|---|
method |
string |
(required) | HTTP method |
url |
string |
(required) | Request URL |
options |
array |
[] |
Symfony HttpClient options |
retryOptions |
array|Closure |
[] |
Options merged on retry, or Closure receiving (string $key, int $retries, ExceptionInterface|InvalidResponseException $e) |
throwOnExhausted |
bool |
true |
Rethrow exception after retries exhausted |
decodeJson |
bool |
true |
Decode response as JSON |
maxRetries |
int |
0 |
Maximum retry attempts |
retryOnTransportException |
bool |
true |
Retry on transport errors (timeouts, DNS) |
parseResponse |
Closure|null |
null |
Runs on 2xx responses before onSuccess. Receives (string $key, int $retries, mixed $result, ResponseInterface $response); return value replaces the result. Throw InvalidResponseException to retry. |
Important
The user_data option is reserved for internal key correlation — passing it in options or retryOptions throws InvalidArgumentException.
Upgrading
See the upgrade/ folder for migration guides between major versions.
License
MIT — see LICENSE for details.