guzzlehttp/ring
A simple handler stack for Guzzle that lets PHP developers compose HTTP client middleware such as retries, redirects, cookies, and request history. Lightweight and easy to integrate, it helps customize request and response behavior in existing Guzzle clients with minimal overhead.
First, the honest context: this package is archived and deprecated. RingPHP was the handler layer behind Guzzle 4/5; Guzzle 6 replaced it with HandlerStack + PSR-7 middleware. With 0 dependents and a last release in 2018, it is not a candidate for new code. The realistic reason to read this assessment is that you're maintaining a legacy Guzzle 5 application, a library that still exposes a Ring handler, or you need to understand an old stack before migrating off it.
Composer install (historical package name is guzzlehttp/ringphp):
composer require guzzlehttp/ringphp
Look here first:
GuzzleHttp\Ring\HandlerInterface — the entire contract is one method: __invoke(array $request): array (or a future).GuzzleHttp\Ring\Core — the utility belt: wrap(), url(), response(), exception(), header helpers. This is where 90% of the semantics live.GuzzleHttp\Ring\Client\CurlHandler, CurlMultiHandler, StreamHandler, MockHandler — the shipped transports.GuzzleHttp\Ring\Future\FutureArrayInterface — then(), wait(), realized().Minimal first request (most people reached Ring indirectly through Guzzle, but you can drive a handler directly):
use GuzzleHttp\Ring\Client\CurlHandler;
$handler = new CurlHandler();
$response = $handler([
'http_method' => 'GET',
'uri' => 'https://httpbin.org/get',
'headers' => ['Accept' => ['application/json']],
'body' => null,
]);
if ($response['error'] !== null) {
throw $response['error'];
}
echo $response['status']; // 200
echo stream_get_contents($response['body']); // body is a stream-ish
Requests and responses are plain arrays. That is the defining feature — and the biggest source of friction when you move to PSR-7.
Middleware via Core::wrap(). This is the canonical composition primitive. You pass a handler and one or more middleware callables; each middleware receives the next handler and returns a new callable:
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Client\CurlHandler;
$handler = Core::wrap(
new CurlHandler(),
function ($handler) {
return function (array $request) use ($handler) {
$request['headers']['X-Trace-Id'] = [bin2hex(random_bytes(8))];
return $handler($request);
};
},
function ($handler) {
return function (array $request) use ($handler) {
$start = microtime(true);
$response = $handler($request);
error_log(sprintf('%s took %.3fs', $request['uri'], microtime(true) - $start));
return $response;
};
}
);
Order matters: the first middleware is the outermost layer, so it sees the request first and the response last.
Handler swapping for tests. MockHandler takes a queue (or callable) of canned response arrays and records what it received. Because handlers are just callables, this is dependency-free — no HTTP server, no container:
use GuzzleHttp\Ring\Client\MockHandler;
$mock = new MockHandler(['status' => 200, 'body' => fopen('php://temp', 'r+')]);
$client = new \GuzzleHttp\Client(['handler' => $mock]);
Async with CurlMultiHandler. Set 'client' => ['future' => true] in the request (or have the client do it) and you get back a FutureArrayInterface. Do useful work, then wait():
$handler = new CurlMultiHandler();
$a = $handler(['http_method' => 'GET', 'uri' => 'https://a.example/', 'client' => ['future' => true]]);
$b = $handler(['http_method' => 'GET', 'uri' => 'https://b.example/', 'client' => ['future' => true]]);
$ra = $a->wait();
$rb = $b->wait();
Internally the multi handler needs tick() calls to progress; wait() on a future from a CurlMultiHandler drives it, but if you're doing manual loops, remember to call $handler->tick().
Environment-based transport selection. Common legacy pattern: use CurlHandler/CurlMultiHandler when the cURL extension is present, StreamHandler (PHP wrappers) otherwise. Keep that decision in one factory function rather than sprinkling extension_loaded('curl') checks.
Bridging to PSR-7. When a modern component needs to consume a Ring handler, write an adapter: translate ServerRequestInterface/RequestInterface → Ring request array (http_method, uri, headers as name => [values], body), invoke the handler, then rebuild a PSR-7 response from status, headers, body, reason. Keep effective_url and transfer_stats on the new response as attributes; they're often the only way to debug redirects and timings.
Capturing traffic for debugging. There is no built-in history middleware. Roll one: a middleware that appends [$request, $response] to a local array (careful with bodies — clone/copy or buffer them, see gotchas).
$response['error'] is the source of truth, not status. On a failed request, status can be null or a stale curl code and error holds the exception/cURL error. Always check error before branching on status.$response['body'] with stream_get_contents() consumes it. If you need the body in middleware and the caller needs it, buffer it into a php://temp and rewind, or you'll hand downstream code an empty stream.name => [values] with array values. Forget the inner array and cURL will silently send something unexpected. Use Core::header()/Core::headers() helpers rather than hand-rolling.StreamHandler is not a drop-in for CurlHandler. No true async (futures resolve immediately), different timeout semantics (timeout vs. connect_timeout behave differently per transport), and transfer_stats keys differ. Tests that pass on cURL can fail on streams.FutureArrayInterface, an error only appears when you wait()/resolve. Exceptions thrown inside then() are easy to swallow — always attach an error path or resolve explicitly in a finally.Core::wrap left-to-right as onion layers: first = outermost. Auth middleware that must run after redirect handling (or before) depends on this.transfer_stats is the debugging goldmine. total_time, namelookup_time, connect_time, redirect_count come straight from cURL — log them in a middleware before you reach for a packet capture.Ring\HandlerInterface → GuzzleHttp\HandlerStack; Core::wrap middleware → Middleware::mapRequest/mapResponse or a custom function (callable $handler) taking (RequestInterface, array $options); array request/response → PSR-7. The shape is deliberately similar, so a Ring middleware usually translates in an afternoon — but the array-to-object change means no mechanical copy-paste.How can I help you explore Laravel packages today?