HTTP request batch executor with individual retries, built on Symfony HttpClient.
Fire multiple HTTP requests in parallel. Each request retries independently without blocking others.
- PHP 8.4+
- Symfony HttpClient 8.0+
composer require shoxcie/batch-http-clientuse 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$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.
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().
$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();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,
)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,
)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.
Pass any HttpClientInterface implementation:
use Symfony\Component\HttpClient\HttpClient;
$httpClient = HttpClient::create([
'timeout' => 10,
'max_duration' => 30,
]);
$client = new BatchHttpClient($httpClient);| 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.
See the upgrade/ folder for migration guides between major versions.
MIT — see LICENSE for details.