Tiny zero-dependency PSR-18 HTTP client backed by ext-curl, with a retry / header-injection middleware pipeline.
A small PSR-18 HTTP client backed by ext-curl, with a composable middleware pipeline (retry on selected status codes, header injection). It speaks PSR-7 requests + responses end-to-end and consumes PSR-17 factories, so any PSR-7 implementation (nyholm/psr7, guzzlehttp/psr7, slim/psr7, …) plugs in as the message layer. The package does not ship its own PSR-7 implementation — bring whichever you already use.
- PSR-18
ClientInterfaceoverext-curl— drop-in anywhere a PSR-18 client is expected. - Composable middleware
Pipeline(itself a PSR-18 client). RetryMiddleware— exponential backoff on configurable status codes and network errors.HeaderInjectionMiddleware— stamp API keys /User-Agent/ telemetry headers on every request.- Network failures surface as a PSR-18
NetworkExceptionInterfacecarrying the failed request. - PSR-7 / PSR-17 consumed via interfaces — bring your own implementation.
- PHPStan level 9 clean,
strict_types,finalclasses.
Guzzle is excellent but heavy — it pulls in guzzlehttp/psr7, guzzlehttp/promises, and its own promise/handler machinery. When all you need is a PSR-18 client that does retries and header injection, this package is a tiny, zero-runtime-dependency alternative: just ext-curl plus the PSR interface packages. You supply the PSR-7 implementation you already have, so there is no duplicate message library in your dependency tree.
composer require amashukov/http-client-php nyholm/psr7(Substitute nyholm/psr7 for the PSR-7 implementation of your choice.)
useAmashukov\HttpClient\CurlClient;
useNyholm\Psr7\Factory\Psr17Factory;
$factory = newPsr17Factory();
$client = newCurlClient($factory, $factory);
$request = $factory->createRequest('GET', 'https://api.example.test/data');
$response = $client->sendRequest($request);
echo$response->getStatusCode(); // 200echo (string) $response->getBody(); // raw bodyCurlClient implements Psr\Http\Client\ClientInterface; it is a drop-in replacement anywhere a PSR-18 client is expected. The constructor takes a Psr\Http\Message\ResponseFactoryInterface and a Psr\Http\Message\StreamFactoryInterface so the caller controls which PSR-7 implementation backs the returned ResponseInterface.
Network failures (DNS, connect timeout, TLS handshake, …) surface as Amashukov\HttpClient\Exception\TransportException, which implements Psr\Http\Client\NetworkExceptionInterface (and therefore ClientExceptionInterface); getRequest() returns the request that failed.
Pipeline wraps any PSR-18 client with an ordered list of middlewares, and is itself a PSR-18 client.
useAmashukov\HttpClient\Pipeline;
useAmashukov\HttpClient\Middleware\HeaderInjectionMiddleware;
useAmashukov\HttpClient\Middleware\RetryMiddleware;
$client = newPipeline(
newCurlClient($factory, $factory),
[
newHeaderInjectionMiddleware(['X-Api-Key' => $apiKey]),
newRetryMiddleware(
maxAttempts: 3,
retryStatusCodes: [429, 502, 503, 504],
baseDelayMs: 200,
),
],
);
$response = $client->sendRequest($request);Implement Amashukov\HttpClient\MiddlewareInterface:
useAmashukov\HttpClient\MiddlewareInterface;
usePsr\Http\Message\RequestInterface;
usePsr\Http\Message\ResponseInterface;
finalclass LoggingMiddleware implements MiddlewareInterface
{
publicfunctionhandle(RequestInterface$request, callable$next): ResponseInterface
{
$start = microtime(true);
$response = $next($request);
printf("%s %s -> %d (%.0fms)\n",
$request->getMethod(),
$request->getUri(),
$response->getStatusCode(),
(microtime(true) - $start) * 1000,
);
return$response;
}
}Call $next($request) exactly once for normal flow, or zero times to short-circuit with a synthetic response.
HeaderInjectionMiddleware — sets the configured headers on every outbound request, replacing any caller-supplied value for the same name. Useful for API keys, User-Agent, telemetry headers.
RetryMiddleware — retries on configured status codes (default [429, 502, 503, 504]) and on any thrown Psr\Http\Client\NetworkExceptionInterface. Delay is exponential (baseDelayMs * 2^(attempt-1)); baseDelayMs=0 means immediate retry. Returns the last response when all attempts are exhausted; rethrows the last network exception when every attempt failed at the transport layer. A $sleeper callable can be injected to control timing in tests.
- PHP 8.3+
ext-curlpsr/http-client^1.0psr/http-message^2.0psr/http-factory^1.1- A PSR-7 implementation (caller-provided)
- PSR-18 —
CurlClientandPipelineimplementPsr\Http\Client\ClientInterface;composer.jsondeclaresprovide: { psr/http-client-implementation: "1.0" }. - PSR-7 — every public method exchanges
Psr\Http\Message\RequestInterface/ResponseInterface. - PSR-17 —
CurlClientconsumesResponseFactoryInterface+StreamFactoryInterface; it does not implement those factories itself, leaving the choice of PSR-7 implementation to the caller.
| Package | Tier | Purpose |
|---|---|---|
| amashukov/toncenter-client-php | RPC | toncenter v2/v3 API client (uses this client) |
| amashukov/eth-rpc-client-php | RPC | Ethereum JSON-RPC client (uses this client) |
| amashukov/ton-php | meta | TON umbrella package |
| amashukov/eth-php | meta | EVM umbrella package |
- PHPStan level 9.
- php-cs-fixer with the
@PER-CSruleset. - GitHub Actions CI on every push.
- PSR-18 conformance exercised in the test suite against a PSR-7 implementation.
MIT.