kinetis / revolt-http-client
A Revolt-native, Fiber-suspending implementation of Symfony's HttpClientInterface — usable standalone by any project (AsyncAws or otherwise), not only Kinetis.
Requires
- php: ^8.4
- amphp/http-client: ^5.3.6
- revolt/event-loop: ^1.0.9
- symfony/http-client: ^8.1.4
- symfony/http-client-contracts: ^3.7.1
Requires (Dev)
- infection/infection: ^0.35.0
- kinetis/framework: ^1.8.1
- phpstan/phpstan: ^2.2.8
- phpunit/phpunit: ^13.3.3
- vimeo/psalm: ^6.17
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
kinetis/revolt-http-client
A Revolt-native, Fiber-suspending implementation of Symfony's HttpClientInterface
Part of Kinetis, a non-blocking PHP framework for API-first applications, developed in the kinetis-dev/kinetis monorepo.
Built for Kinetis, but usable in any PHP project — this package depends
on nothing beyond symfony/http-client (and its
symfony/http-client-contracts), amphp/http-client, and
revolt/event-loop. No kinetis/framework required.
use Kinetis\RevoltHttpClient\Http; $api = new Http()->withBaseUrl('https://api.example.com')->withToken($key); $orders = $api->get('/orders', ['status' => 'open'])->throw()->json(); $api->post('/orders', ['sku' => 'A1', 'quantity' => 2]);
A request made through this client suspends the calling Fiber and yields
back to Revolt's event loop while waiting on the network, instead of
blocking the whole process — so several run at once through
Kinetis\Async\concurrently() with no pooling API of its own.
What it guarantees
- A credential is pinned to one origin. A client carrying an
AuthorizationorCookieheader requireswithBaseUrl(), and then every URL it accepts is relative to that base. Another origin is another client. The validated URL alone names where a request goes:Host,Proxy-Authorization, andAccept-Encodingare refused wherever a caller writes them. - Redirects are never followed. A 3xx is a terminal response with a
Locationto read. Following one means deciding, per response, whether a new origin may see this client'sAuthorizationheader, cookies, and body — a decision belonging to the caller who knows what the credential is for. - One retry layer, for idempotent methods only, and one total
deadline.
withRetries()is the only way to configure retries, the transport underneath makes one wire attempt per request, and a per-call retry option is refused rather than merged. Retries apply only to exactlyGET,HEAD,OPTIONS,TRACE,PUT, andDELETE; every other method,POSTandPATCHincluded, is sent once, because neither a dropped connection nor a retryable status proves the server did not apply it. A stream orClosurebody is refused on a request that could be retried, rather than resent.withTimeout()bounds the whole operation on a monotonic clock — every attempt, every backoff, and every read of the response — and is enforced here rather than trusted to the transport. - A bounded response.
withMaxResponseBytes()is the ceiling a body may reach, enforced while it arrives rather than once it is already in memory. Every request asks for identity encoding, so the bytes counted are the bytes held rather than the bytes off the wire. - Failures carry no secrets.
HttpRequestException— the one exception type this package throws — carries the request method, the origin, a status, and a category from a fixed list. No path, no query string, no header, no credential, no body, and no vendor exception chained behind it.
An error status is not one of those failures: successful(), failed(),
clientError(), serverError(), and redirect() are answers to branch
on, and throw() opts into raising instead. Read the body with json(),
jsonPath('customer.email'), or body().
AmpHttpClientFactory::create() returns the underlying Symfony
HttpClientInterface on its own, for libraries that want to be handed a
client. It is a plain Symfony client and the escape hatch: none of the
guarantees above apply to it, redirect following included. One
thing carries across, and it is what Http itself is built on: one
request is one wire attempt, so a library given this client retries on
its own terms or not at all.
use Kinetis\RevoltHttpClient\AmpHttpClientFactory; $client = AmpHttpClientFactory::create(); $response = $client->request('GET', 'https://example.com/');
Using it with AsyncAws
Every AsyncAws client — S3, SQS, SES, DynamoDB, or any of its other
services — extends AsyncAws\Core\AbstractApi, whose constructor takes
an optional ?HttpClientInterface $httpClient:
use AsyncAws\S3\S3Client; use Kinetis\RevoltHttpClient\AmpHttpClientFactory; $s3 = new S3Client(['region' => 'us-east-1'], null, AmpHttpClientFactory::create());
Nothing about this is Kinetis-specific — the same pattern works for any
other library that accepts an injectable HttpClientInterface.
Installation
composer require kinetis/revolt-http-client
Requires PHP 8.4+ (the floor symfony/http-client:^8.0 itself requires).
Full documentation:
kinetis.dev/docs/revolt-http-client.html.
License
MIT — see LICENSE.